Warning: you need to understand metatables and, possibly, IEEE754 arithmetics to understand this post. If you don’t, just skip it
Background
In Lua 5.1, the semantics of the equality checks (==
and ~=
) are such that when an object that’s being compared has an __eq
metamethod, the metamethod is only called when the object isn’t compared with itself. In other words, a == b
works approximately as rawequal(a, b) or getmetatable(a).__eq(a, b)
. This means that __eq
isn’t called when you compare an object with itself.
This has been true of Roblox Lua implementation as well, but accidentally was changed in Luau when it was released in 2019 for a particular case of builtin userdata comparison - for Vector3
, CoordinateFrame
and other objects like that v == v
was in fact calling the built-in __eq
metamethod.
This is significant as since 2019, games started using this to compare Vector3
objects to itself to check for NaN values (v ~= v
is true if any component of the vector is NaN). You may think this is a hack, but it actually makes sense - the following code always was a correct way to check if any component of a Vector3
is a NaN:
a.X ~= a.X or a.Y ~= a.Y or a.Z ~= a.Z
If you’re confused by this, it’s alright - NaN semantics can be confusing! But very smart people in 1985 decided that this is a good idea, and pretty much all computers and programming languages today implement this semantics for floating-point computation.
This brings us to:
What changed?
As of today, we’re making this behavior official and consistent. Instead of only calling __eq
for self comparisons in rare cases, we’re now always calling __eq
even if the object is compared with itself.
Why is it a good idea?
This change removes the difference in semantics between builtin types and custom table-based types. This means that if you were to implement a Vector5
type by using a table with a metatable, you’re going to get the same behavior wrt equality comparison as Vector3
. This makes builtin types less special, which is great.
It also keeps the existing games working. We initially wanted to revert the behavior back to be in line with Lua 5.1, but existing games had code that wasn’t compatible with this change. And the existing situation was very inconsistent, so we had to change it one way or the other - this way should be less disruptive than alternatives while still resulting in a consistent comparison behavior.
It also just makes sense. For example, Lua 5.1 won’t call __eq
when you ask it v == v
, but will call __le
when you ask if v <= v
. It makes more sense to treat all types and operators uniformly without magical behavior.
This change is effective as of April 9th.
Will my game still work?
Yes!
… almost definitely.
We’ve had some reports of games that may be affected by this when they incorrectly implemented __eq
metamethod but the problem happened to never occur.
For example, consider this code:
getmetatable(t).__eq = function (a, b)
return a == b
end
This code is definitely not really necessary, but is it correct? The answer is - no! The ==
operator inside the __eq
implementation will just call __eq
again, resulting in a stack overflow. The correct implementation here is return rawequal(a, b)
.
However, if the code above was never called - which could happen if you only ever compared objects of this type redundantly to itself - the error could have been hidden in plain sight, and now it’s visible - because before this change we wouldn’t call the __eq
when you compared an object of this type to itself (t == t
), and now we will.
Please let us know if there are other unintended cases like this; we currently believe that all correct code is not affected. Needless to say, if your game never implements __eq
it is not affected by this at all.