Tuesday 31 December 2013

(2/4) ColdFusion: var hoisting can make a difference

G'day:
Remember a while back I observed that - like JavaScript - ColdFusion hoists its VAR declarations: "ColdFusion hoists VAR declarations". At the time I could not see how this could cause problems in CFML. Well I've noted a case where it does cause problems, in that it reveals a bug in ColdFusion 9 & 10.

I came across this ticket on the bug tracker: "cfloop array and wrong iterator", which details how ColdFusion 9 (and, as it turns out: 10) gets confused whilst array-looping in an edge-case situation. Check this out:


<!--- hoist.cfm --->
<cffunction name="test" output="true">
    <cfset var colours = ["whero","karaka","kowhai","kakariki","kikorangi","tawatawa","mawhero"]>

    #serializeJson(colours)#<br>

    <cfloop array="#colours#" index="colour">
        #colour#<br>
    </cfloop>

    <cfset var colour = "">
</cffunction>
  
<cfset test()>

Running this gives...

["whero","karaka","kowhai","kakariki","kikorangi","tawatawa","mawhero"]
whero
whero
whero
whero
whero
whero
whero


Didn't expect that, did you? Me neither. Running it on Railo gives more predictable results:

["whero","karaka","kowhai","kakariki","kikorangi","tawatawa","mawhero"]
whero
karaka
kowhai
kakariki
kikorangi
tawatawa
mawhero


Initially I thought it was yet another bug with array-literal notation (a lot of bugs have been raised in CF9 and CF10 regarding struct-literal notation: all of them also apply to array-literal notation), so I refactored that out of the code:

<!--- noLiteral.cfm --->
<cffunction name="test" output="true">
    <cfset var colours = arrayNew(1)>
    <cfset colours[1] = "whero">
    <cfset colours[2] = "karaka">
    <cfset colours[3] = "kowhai">
    <cfset colours[4] = "kakariki">
    <cfset colours[5] = "kikorangi">
    <cfset colours[6] = "tawatawa">
    <cfset colours[7] = "mawhero">
    #serializeJson(colours)#<br>

    <cfloop array="#colours#" index="colour">
        #colour#<br>
    </cfloop>

    <cfset var colour = "">
</cffunction>
  
<cfset test()>

Same result.

The next of the bug in the bug tracker gave a clue: "When you var scope the index anywhere in your function the index item will always be the first item of the array." This is not actually correct, but it got me thinking about the code. Look where colour is being VARed. After it's being used. I revised the code to VAR the thing in a more sensible place:

<!--- noHoist.cfm --->
<cffunction name="test" output="true">
    <cfset var colour = "">
    <cfset var colours = ["whero","karaka","kowhai","kakariki","kikorangi","tawatawa","mawhero"]>

    #serializeJson(colours)#<br>

    <cfloop array="#colours#" index="colour">
        #colour#<br>
    </cfloop>

</cffunction>
  
<cfset test()>

And now the output is correct (as per above).

Huh?

I decompiled the two files, to see if there was anything obvious, but no:

  protected final Object runFunction(LocalScope __localScope, Object instance, CFPage parentPage, ArgumentCollection __arguments)
  {
    Object value;
    parentPage.bindImportPath("com.adobe.coldfusion.*");
    Variable ARGUMENTS = __localScope.bindInternal(Key.ARGUMENTS, __arguments);
    Variable THIS = __localScope.bindInternal(Key.THIS, instance);
    Variable COLOUR = __localScope.bindInternal("COLOUR");
    Variable COLOURS = __localScope.bindInternal("COLOURS");
    JspWriter out = parentPage.pageContext.getOut();
    Tag parent = parentPage.parent;
    parentPage._whitespace(out, "\r\n\t");
    Variable ___IMPLICITARRYSTRUCTVAR0 = __localScope.bindInternal("___IMPLICITARRYSTRUCTVAR0");
    ___IMPLICITARRYSTRUCTVAR0.set(parentPage.ArrayNew(1));
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "1" }, "whero");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "2" }, "karaka");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "3" }, "kowhai");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "4" }, "kakariki");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "5" }, "kikorangi");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "6" }, "tawatawa");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "7" }, "mawhero");
    COLOURS.set(parentPage._get(___IMPLICITARRYSTRUCTVAR0));
    parentPage._whitespace(out, "\r\n\r\n\t");
    parentPage._setCurrentLineNo(5);
    out.write(parentPage.SerializeJSON(parentPage._autoscalarize(COLOURS)));
    out.write("<br>\r\n\r\n\t");
    List t13 = Cast._List(parentPage._autoscalarize(COLOURS));
    int t14 = 1;
    int t15 = 0;
    int t16 = t13.size();
    for (Variable t17 = parentPage.bindPageVariable("COLOUR", __localScope); t15 < t16;    t15 += t14) {
        value = t13.get(t15);
        t17.set(value);
        if (value != null) {
            parentPage._whitespace(out, "\r\n\t\t");
            out.write(Cast._String(parentPage._autoscalarize(COLOUR)));
            out.write("<br>\r\n\t");
        }
    }
    parentPage._whitespace(out, "\r\n\r\n\t");
    COLOUR.set("");
    parentPage._whitespace(out, "\r\n");
    return null;
  }


 protected final Object runFunction(LocalScope __localScope, Object instance, CFPage parentPage, ArgumentCollection __arguments)
  {
    Object value;
    parentPage.bindImportPath("com.adobe.coldfusion.*");
    Variable ARGUMENTS = __localScope.bindInternal(Key.ARGUMENTS, __arguments);
    Variable THIS = __localScope.bindInternal(Key.THIS, instance);
    Variable COLOUR = __localScope.bindInternal("COLOUR");
    Variable COLOURS = __localScope.bindInternal("COLOURS");
    JspWriter out = parentPage.pageContext.getOut();
    Tag parent = parentPage.parent;
    parentPage._whitespace(out, "\r\n\t");
    COLOUR.set("");
    parentPage._whitespace(out, "\r\n\t");
    Variable ___IMPLICITARRYSTRUCTVAR0 = __localScope.bindInternal("___IMPLICITARRYSTRUCTVAR0");
    ___IMPLICITARRYSTRUCTVAR0.set(parentPage.ArrayNew(1));
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "1" }, "whero");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "2" }, "karaka");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "3" }, "kowhai");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "4" }, "kakariki");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "5" }, "kikorangi");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "6" }, "tawatawa");
    parentPage._arraySetAt(___IMPLICITARRYSTRUCTVAR0, new Object[] { "7" }, "mawhero");
    COLOURS.set(parentPage._get(___IMPLICITARRYSTRUCTVAR0));
    parentPage._whitespace(out, "\r\n\r\n\t");
    parentPage._setCurrentLineNo(6);
    out.write(parentPage.SerializeJSON(parentPage._autoscalarize(COLOURS)));
    out.write("<br>\r\n\r\n\t");
    List t13 = Cast._List(parentPage._autoscalarize(COLOURS));
    int t14 = 1;
    int t15 = 0;
    int t16 = t13.size();
    for (Variable t17 = parentPage.bindPageVariable("COLOUR", __localScope); t15 < t16;    t15 += t14) {
        value = t13.get(t15);
        t17.set(value);
        if (value != null) {
            parentPage._whitespace(out, "\r\n\t\t");
            out.write(Cast._String(parentPage._autoscalarize(COLOUR)));
            out.write("<br>\r\n\t");
        }
    }
    parentPage._whitespace(out, "\r\n\r\n");
    return null;
  }

The highlighted bit is the only relevant difference in the two versions. There are other differences, but they relate to debug line numbering and spurious whitespace output.

Weird. Obviously(?) it's linked to how this bit works:

List t13 = Cast._List(parentPage._autoscalarize(COLOURS));
int t14 = 1;
int t15 = 0;
int t16 = t13.size();
for (Variable t17 = parentPage.bindPageVariable("COLOUR", __localScope); t15 < t16;    t15 += t14) {
    value = t13.get(t15);
    t17.set(value);

But I don't see how the setting of t17 can be impacted by the presence or absence of COLOUR being set() subsequent to that?!

Also note that this is a peculiarity of an array loop: it does not happen with this version of the code:

<!--- indexedLoop.cfm --->
<cffunction name="test" output="true">
    <cfset var colours = ["whero","karaka","kowhai","kakariki","kikorangi","tawatawa","mawhero"]>

    #serializeJson(colours)#<br>

    <cfloop index="i" from="1" to="#arrayLen(colours)#">
        <cfset colour = colours[i]>
        #colour#<br>
    </cfloop>

    <cfset var colour = "">
</cffunction>

<cfset test()>

It's also not a problem with the same code in CFScript:

// script.cfm
function test() output=true {
    var colours = ["whero","karaka","kowhai","kakariki","kikorangi","tawatawa","mawhero"];

    writeOutput("#serializeJson(colours)#<br>");

    for (colour in colours){
        writeOutput("#colour#<br>");
    }

    var colour = "";
}

test();

Weird.

Not entirely useful to know, but weird.

--
Adam