Change evaluation order of compound assignments

As a Roblox developer, the order in which compound assignments are evaluated is unintuitive. In Luau, the value of a variable being assigned to with a compound assignment is evaluated after the evaluation of the right operand of the compound assignment; however, the opposite ordering is done if the assignment is being done to a table or if the variable is not local to the function. For example:

-- local variable
local x = 0
local function f()
	x = 1
	return x
end
x += f()
print(x) -- 2
-- non-local variable
x = 0
(function()
	x += f()
end)()
print(x) -- 1
-- table (also applies to global variables)
local y = {0}
local function g()
	y[1] = 1
	return y[1]
end
y[1] += g()
print(y[1]) -- 1

The order that compound assignments with tables or non-local variables are performed is inconsistent with the order when using compound assignment with local variables. It would make more sense if the value was obtained from the table or variable after evaluating the right hand side of the compound assignment. Here is a more complicated example which demonstrates the current evaluation order and what this change would look like:

local t
t = setmetatable({},{
	__index = function()
		print"__index"
		return t
	end,
	__newindex = function()
		print"__newindex"
	end,
	__add = function()
		print"__add"
		return t
	end
})
t[print"left hand side 1"][print"left hand side 2"] += print"right hand side"

Output

left hand side 1
__index
left hand side 2
-__index
right hand side
+__index
__add
__newindex

The second call to __index would be moved after the evaluation of the right hand side, but the evaluation of the left hand side expression would otherwise be unchanged. With this change and making non-local variables have the same evaluation order as local variables, the original example would print 2 three times (which is more consistent). I prefer the current behavior of compound assignment with local variables because it allows for examples like the following to work:

x += task.wait(1)
-- increments x by the amount of time that was yielded
-- this works even if x was changed by something else during the call
-- if the other order is desired, that could be done with x = x+task.wait(1)

It is also consistent with how languages such as C and C++ will evaluate compound assignment. Regardless of which order is chosen, the order should ideally be consistent between local variables, non-local variables, and tables (including global variables).

3 Likes

I don’t think we are going to be changing this.

In general, ordering of effects in assignments is not guaranteed when dependencies are not syntactically visible in Luau or Lua:

If a function or a metamethod called during the assignment changes the value of a variable, Lua gives no guarantees about the order of that access.
(5.4 reference manual)

I suggest writing code without such hidden effects, it will make clear what’s happening to whoever is reading the code later (you can even surprise your future self).

2 Likes

I don’t see how this quote is relevant here. The full section which this is a part of is:

If a variable is both assigned and read inside a multiple assignment, Lua ensures that all reads get the value of the variable before the assignment. Thus the code

i = 3
i, a[i] = i+1, 20

sets a[3] to 20, without affecting a[4] because the i in a[i] is evaluated (to 3) before it is assigned 4. Similarly, the line

x, y = y, x

exchanges the values of x and y, and

x, y, z = y, z, x

cyclically permutes the values of x, y, and z.

Note that this guarantee covers only accesses syntactically inside the assignment statement. If a function or a metamethod called during the assignment changes the value of a variable, Lua gives no guarantees about the order of that access.

To me, this reads that if there is a multiple assignment then referencing the value of one of the assigned variables from another part of the assignment (i.e. other than the expression that names the variable being assigned to and the expression that is the value being assigned to the variable) inside of a metamethod or function may read the values before or after the assignment. For example:

local x,y = 1,2
local function f()
	return x
end
local function g()
	return y
end
x,y = g(),f()
print(x,y)

Whether g reads the value before the assignment to y and whether f reads the value before the assignment to x is unspecified. However, something like this would be defined:

local x
local function f()
	x = 1
	return 2
end
x = f()
print(x) -- always outputs 2

Compound assignment is always a single assignment, so this text shouldn’t be relevant here. What’s in question is whether a += b() should load the value of a or call b first. It seems pretty clear that the call to b is going to happen before the assignment to a.

3 Likes

I agree, this is surprising behavior, and that 5.4 manual snippet is obviously talking about multiple assignment order.

I always thought x += f() acted the same as:

local y = f()
x = x + y

Which would always yield 2 for the original conditions.

1 Like