Monday, 2 May 2022

CFML: with Lucee, true isn't necessarily the same as true

G'day:

Yesterday I decided to improve my "Tiny Test Framework". I wrote an article about developing the first iterationof this as a TDD exercise six or so months ago: "TDD: writing a micro testing framework, using the framework to test itself as I build it". I use this framework on trycf.com, so I can include tests in my code samples. The first iteration of this only had the one toBe matcher, and yesterday I decided to add in toBeTrue, toBeFalse and toThrow: just to make my sample code on trycf a bit clearer.

Whilst implementing the toBeTrue matcher, I had a brain fart and ended up with this:

if (actual === true) {
    return true
}

This was the wrong approach to start with, and I'm too explicit in my check. I don't wanna be using the === operator there, this should be sufficient:

if (actual) {
    return true
}

IN CFML "truthy" should be enough for this matcher, as that's idiotmatic-CFML.

But whilst running my tests, I noticed something weird in Lucee. I had a test along these lines:

it("a passing test", () => {
    x = "something"
    expect(x.equals(x)).toBeTrue()
})

On ColdFusion, this passed fine.

But on Lucee, it failed. It seemed that apparently the same string doesn't equal itself. Although the more I dug, the weirder it got. I distilled a repro case down to:

s = "there can be only one"    
writeOutput(s.equals(s) === true) // Lucee: false; ColdFusion: true

This completely flummoxed me, and I posted something on the CFML Slack channel about it. Jonas Eriksson pointed me in the direction of an article on the topic of === that Ben had written a coupla years ago("Exploring The Triple Equals (===) Operator In Lucee CFML 5.3.4.77"). The article itself didn't tell me anything I didn't already "know" (ahem), but Brad's comment reminded me that Lucee's implementation of === is a bit shit. There's a Jira ticket for it here: "LDEV-1282: Fix === operator to check for type equality", and another one "LDEV-3132: ACF2021 - Identity/Not Identity Operator" that I have even participated in. Just a week ago!!.

The issue is that Lucee has decided to make the === operator to be an identity operator. IE: it does not do an equality and type check like ColdFusion does (full disclosure: Lucee implemented this operator before ColdFusion did), it just checks that the two operands are exactly the same object (ie: in memory).

How is this causing my problem? Well it's because not all trues are created equal, in Lucee. This gist demonstrates it:

system=createObject("java", "java.lang.System")    
    
s = "there can be only one"

e = s.equals(s)

t = true

writeDump([
    "e" = e, // true
    "t" = t, // true
    "==" = e == t, // true
    "===" = e === t, // LUCEE: FALSE; ColdFusion: true
    "e hashcode" = system.identityHashCode(e), // one thing, eg 712659136
    "t hashcode" = system.identityHashCode(t) // a different thing, eg: 558076530
])

Notice how the identity hashes for e and t are different. Therefore e === t is false on Lucee. This, I'm afraid, makes the === operator on Lucee about as useful as a chocolate teapot. In CFML it's really not important that a value occupies the same memory; but sometimes it's handy - because CFML is a loosely- & dynamically-typed language - to be able to check a value is the same and that it's actually the same type too (eg:"1" is equal to 1, but is not the same type). And this lack of joined-up-thinking in Lucee is demonstrated pretty clearly in that two instances of true are not considered the same as each other. That's just daft.

Out of interest I decided to push the boat out on my largely non-existent Java skills, check what Java does here. I came up with this:

class TestIdentiyEquality {

    public static void main(String[] args) {
        Boolean b1 = new Boolean(true); // deprecated approach to getting a Boolean
        Boolean b2 = new Boolean(true);

        System.out.println(String.format("via constructor, using equals: %b", b1.equals(b2))); // true 
        System.out.println(String.format("via constructor, using ==: %b", b1 == b2)); // false

        Boolean b3 = Boolean.parseBoolean("true");
        Boolean b4 = Boolean.valueOf("true");

        System.out.println(String.format("via parseBoolean/valueOf, using equals: %b", b3.equals(b4))); // true 
        System.out.println(String.format("via parseBoolean/valueOf, using ==: %b", b3 == b4)); // true
    }

}

It's interesting that in Java, creating a new Boolean object via its constructor is deprecated these days, in favour of using static methods that return the same Boolean each time. I guess this is Lucee's problem too: its true isn't the same object as the one the JVM is using (I presume internally Java uses the same object all over, but can't be arsed thoroughly checking this). Either way, Lucee have claimed that they have followed Java's route for the identity operator, which I think is odd given Java doesn't distinguish between value equality and identity equality via operators; it uses the equals method for checking value, and it only has the == operator other than that, and that does do an identity check. Also Java has no need to differentiate between "value equality" and "value and type equality" because it's a strongly typed language. It's not routine in Java to need to do myStringContaining1 == myNumericContaining1 like one would in a loosely typed language like CFML.

I did a check of some other languages that operate in a similar space to CFML (this is being a bit kind to CFML, but hey). My findings were:

  • Lucee, Python, Groovy, Kotlin treat === as reference equality check
  • CF, PHP, JS, Ruby treat it as a value & type check (in Ruby even == checks type too)

The code for the comparisons is in this gist.

I think CFML more closely fits with PHP, JS and - to a lesser extent - Ruby here, and I think ColdFusion has got the implementation of === right. Lucee should sort theirs out to work the same.


Oh, and where did I get to with my improvements to my tiny testing framework? This Twitter message sums-up my afternoon:

I spent so much time looking at various bugs in Lucee and spilling over into ColdFusion as I checked my code for the Lucee problem and found other bugs on ColdFusion too that I lost the taste for doing CFML, and just drank beer instead. It's a better use of my time, I think. The same could be applied to writing daft blog articles, so I'm off to find a beer instead.

Righto.

--
Adam


Update

OKOK, I did check what Java does about comparing two different true expressions:

class TestEquals {

    public static void main(String[] args) {
        String s1 = "there can be only one";
        String s2 = "maybe there can be another one, actually";
        Boolean b1 = s1.equals(s1);
        Boolean b2 = s2.equals(s2);

        System.out.println(String.format("b1: %b; b2: %b; b1 == b2: %b", b1, b2, b1 == b2)); // b1: true; b2: true; b1 == b2: true
    }

}

The equivalent in CFML is:

s1 = "there can be only one"
s2 = "maybe there can be another one, actually"
b1 = s1.equals(s1)
b2 = s2.equals(s2)

writeOutput("b1: #b1#; b2: #b2#; b1 == b2: #b1 === b2#") // b1: true; b2: true; b1 == b2: false (true on ColdFusion)

Even in the context of booleans "we do it how Java does it" doesn't stand-up to too much scrunity with Lucee. I think its implementation is just wrong.