Call __eq even when tables are rawequal

In custom datatypes (eg BigNum, quaternions), I want to have robustness against nan. But I also want to be able to write code that works for a bunch of datatypes (including Roblox default ones eg numbers and Vector3s). The problem is that the metamethod __eq isn’t called when two variables share the same table memory address, so rather than writing t~=t to check for nan, we have to write and call an extra function. I’m guessing this problem probably also gives rise to sneaky bugs.

The below code shows that while we can use t==t to check if a Vector3 is non-nan, we cannot do the same for tables.

local mt={}
function mt.__eq(t0,t1)
	warn'__eq called'
	return t0[1]==t1[1]
end
function mt.new()
	return setmetatable({0/0},mt)
end

local t=mt.new()
print(t==t,rawequal(t,t),mt.__eq(t,t))--true,true,false, I want it to be false,true,false

local v=Vector3.new(0/0)
print(v==v,rawequal(t,t))--false,true
7 Likes

This would be a useful change, but I don’t think Roblox should make changes to the semantics of the Lua. (if this can be changed, what else can, and what can we really rely on?)

Wouldn’t this also have some performance implications? Every comparison with tables that are primitively equal would now have to check for __eq, which in most cases will either not exist or will confirm that they are equal.

All Roblox data types appear to be treated specially, not just Vector3 (it also applies to Vector2, Region3, UDim, UDim2, Ray, TweenInfo, Rect, etc.). Roblox data types having this special behavior isn’t an incompatibility (and changing it back now would likely be an incompatibility), which makes this change possible for Roblox data types specifically.

Also, for Vector3 specifically rawequal may match the behavior of using the comparison operators in the future.
https://devforum.roblox.com/t/a-lesson-in-over-optimisation-magnitude-calculation/533956/21?u=halalaluyafail3

1 Like

Can you elaborate more on this? As I understand it, it seems that @zeuxcg is saying rawequal(v.P,v.P) is undefined when v is a Roblox instance and P is a property, and I’m not sure how this applies?


Roblox is now making non backwards compatible lua(/luau?) changes eg to xpcall, and personally (as I actively refactor my code) I like this—and I’m guessing this __eq use case is also niche enough to justify a change like the precedence the xpcall change hopefully set.

1 Like

If Vector3 is a value type it shouldn’t need any metamethods, so comparing two Vector3 with comparison operators should be the same as if rawequal was used (like comparing other value types). It being a value type means there isn’t any reference, so it would only make sense to compare by value.

Was it ever guaranteed that xpcall isn’t allowed to yield? The Lua manual defines that __eq will not be tried if the objects being compared are primitively equal, I don’t see anything stating that the function passed to xpcall isn’t allowed to yield. xpcall yielding was also added in later versions of Lua, so it was easier to justify adding. Roblox has made changes to behavior unspecified in the Lua manual (e.g. order of assignment in multiple assignments), but not for defined behavior (e.g. when metamethods get called).

Roblox has added functions to libraries, added globally defined functions, and added extra arguments to functions. Adding these breaks some code, but there isn’t a guarantee that something like table.move is nil.

-- now adding table.move causes an error
if table.move then error"" end
-- adding base argument to mah.log now checks the type
-- and causes an error
math.log(1,{})
3 Likes

Sounds like it would make rawequal change its behavior to what I desire in OP, not the other way around: rawequal(0/0,0/0)==false.


The rest of your argument is neat. I won’t mark the post as resolved though because I still wish for this change to happen, but I now see it is w/o precedent. I understand if an engineer wants to mark this as resolved/discard this suggestion.


No idea

1 Like

If anything the fact that userdata comparison doesn’t start with rawequal check feels slightly odd. Don’t have time to do code archeology atm but this might actually be a Luau bug; I think you can observe this on code like this:

local x = newproxy(true)
local mt = getmetatable(x)
mt.__eq = print
local y = x == x;

For BigNum, do you really need a concept of NaN? In general NaN breaks all sorts of “common sense” rules for relational and equality and as such it’s not clear that we should implement features / change behavior in ways that help further that goal.

6 Likes

Also, our general behavior change policy (loosely defined) is that if some behavior is problematic, changing this is not believed to cause significant breakage in code that runs on Roblox, and the new behavior matches that of later versions of Lua, we are likely to make this change as long as it’s performance neutral.

To the best of our knowledge this was the case with pcall/xpcall (both of these started as not supporting yields on Roblox platform and gradually gained that capability, although xpcall did that 7-something years later than pcall). We learned of cases where xpcall was used to intentionally break yielding after a while but we had many many other cases where that behavior was unintuitive (why are pcall and xpcall different?) and problematic (no workaround for xpcall).

Although in general our policy also says that code that errors today is fair game for behavior changes, as such xpcall failing to yield can change at any time (and just did!), whereas xpcall succeeding to yield can’t change. This is motivated by the fact that it’s incredibly unlikely that a program relies on failure to function correctly and this allows us a rather large room to introduce new features, e.g. technically we changed the behavior of pcall(loadstring, "for i=1,2 do continue end") but that’s fair game per rules above.

6 Likes

I was just about to write a response based on the userdata __eq behavior I remember from a few years ago. I think it changed.

-- should be false
print(CFrame.new(0/0, 0/0, 0/0) == CFrame.new(0/0, 0/0, 0/0))

local foo = CFrame.new(0/0, 0/0, 0/0)
-- should be true (but is now false)
print(foo == foo)

I think a rawequal check would be preferable if it improves overall performance, even if there are some inconsistencies with nan.

I generally avoid using custom datatypes for things like quaternions because the performance is awful compared to doing math directly with the x/y/z/w components. I can understand why this would be frustrating when developing a BigNum class though. You want to create a class that can be used ergonomically like regular types, but I don’t think it’s worth a performance hit to other performance-sensitive use cases for __eq. You can still achieve robustness against nan through something like BigNum.IsNaN(foo).

I’ve developed classes where __eq tests are performance critical. For example, my Lua source simplifier has classes representing Lua values. When a parsed function has multiple possible return values, it can be represented using a “OneOf” subclass; If a function returns the same value representation multiple times it can deduplicate it relatively efficiently by comparing equality; This can be expensive in some cases, but it can improve performance later in the simplification process when it needs to perform tests or operations on each possible result. Using __eq for this along with table.find could greatly improve performance.

In recent years I’ve been avoiding mathematic/equivalence metamethods completely. IMO these are the only useful metamethods:

  • __mode - Used for the unique efficient cleanup behavior. All metamethods can be emulated at the expense of ergonomics, except for __mode.
  • __index - Used for ergonomic classes: foo:bar(), and for ergonomic lazy initialization: foo.bar. For game code I’ve been avoiding this and instead using metatable-less arrays with Class.bar(foo), which has better performance and can be inlined easily. For cases where objects need inheritance, foo[1][1](foo) can have better performance (where foo[1][1] is the method bar), although this is more confusing and less ergonomic.
  • __call - Used for ergonimics: foo(), and to make tables compatible with custom lightweight events that call each listener. This way a single table can be allocated without creating an extra function. This doesn’t work well with the type system though.

I occasionally use others for debugging, but In my experience anything beyond that tends to add unneeded complexity to code.

4 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.