Luau Recap: October 2021

The formatter within the script editor still thinks it’s an if statement without an escape (end) so it moves the next block of code a tab to the right.

2 Likes

I’ve seen a lot of people asking for a different way besides if-then-else, but roblox has many reasons to why this is the case

they have many examples about why if-then-else was chosen via the link

GitHub link

2 Likes

I completely agree. Its an alit better system to use when scripting. This can change a lot in people’s work and most likely shorten the scripts.

2 Likes

Someone else already commented on this above:

The problem is know and planned to be fixed.

3 Likes

Indentation does not happen when the expression is being used to set a variable, however the end autocompletion does:

1 Like
print(if true then false else nil)

image
code works completely fine, no errors in output
just the warn is annoying

i’ve been able to get a lot of warns in the script editor that look a lot like this

--Type 'Type' could not be converted into 'Type'
3 Likes

The indentation and end completion issue in Studio should be fixed now since we enabled support for handling this feature in the editor.

5 Likes

About the typechecker, when can we expect the types returned to be updated? Because currently this will happen:
Screen Shot 2021-11-03 at 8.46.31 AM

I’ve put all kinds of crap into the players service to test this and no matter what it will always only return the player objects.

3 Likes

Roblox types are really poor. I’ve had an extremely consistent experience with Roblox LSP, I’ve never had any issues with type definitions. (Only once with :SetCore)

Also shouldn’t Instance be able to be converted to Player? Is that a strict typing thing?

I wonder if they didn’t make Player the type because it wouldn’t be able to accept children properly. Not sure if Instance even does anyway.

2 Likes

The Roblox typechecker doesn’t automatically replace type’s with with types that inherit from the base, if that’s what you mean. To do that you have to do this:

for _,child : Instance in pairs(instance:GetChildren() do -- Child will default to any, even though :GetChildren only returns instance's. So you have to explicitly define it's type as 'Instance'
	if child:IsA("BasePart") then
		print(child) -- child will now show it's type as 'BasePart' when you hover over it
	end
end

In other words, no, you cannot do this:

for _,child : BasePart in pairs(instance:GetChildren()) do

This is for good reason though, because while BasePart does inherit from Instance, it is only one of the many classes that inherit from it. Because Instance | Roblox Creator Documentation returns all Instance's under the object, not just those with type BasePart, it isn’t compatible.

That isn’t my point however. My point is that Instance:GetChildren returns type {Instance} while Players | Roblox Creator Documentation also returns the type {Instance} even though it should return {Player} since it already filters through the children so that it only returns the children of the type Player.
It explicitly says on the documentation page for Players:GetPlayers: “It functions the same way Instance:GetChildren would except that it only returns Player objects.”

There’s still end completion and indentation issues when using this new syntax. If there is a function in place of either ‘a’ or ‘b’ in if a then b else c it causes issues. Is there any word on if/when we can expect this to be fixed?

2 Likes

Unfreezing a table will never be allowed as it uses the exact same readonly mechanism that the global environment, Instance metatable, and many, many other objects use to protect themselves from malicious modifications. Providing an unfreeze operation would allow these sacred objects to be unprotected.

Plus, the point of freezing is to prevent modification.

3 Likes

What’s the difference between local maxValue = if a > b then a else b and local maxValue = a > b and a or b?

Is it just speed, or are they the same thing, I’ve been doing this for a while…

If both a and b are numbers, they are approximately the same. if then else might be a tiny bit faster.

The reason why we introduced if then else is because if a isn’t a number and can ever be false or nil, the expression doesn’t do what you’d assume it does.

So to be clear, the difference isn’t apparent in this specific example, but it can be apparent in other examples shown in this thread.

5 Likes

Is an optimization for general, non-metatable-based indexing feasible by any means? I don’t know what performance for indexing looks like right now for Luau in its current state (namely if it’s considered “good enough” to just drop such an optimization). My thinking is along the lines of what Luau does for indexing stuff like math.max and how it makes that as fast as (faster? I don’t recall the benchmark) than indexing a local variable.

My main use of immutable structures outside of Luau is for that speed bonus that compilers can leverage, so if Luau can leverage that as well on its own, that would be pretty awesome.

1 Like

It’s approaching “as good as theoretically possible” although not quite there :wink: it’s a very hot path. Curiously, trying to account for freezing on that path would probably actually make it slower…

The core problem with your idea as I understand it is that we need to validate the path and cache the result somewhere; for builtins they are global so there’s only one cache you need, and validation is possible to do very quickly due to our sandboxing. Doing the same on the general table indexing path isn’t super practical.

4 Likes

Would it be possible to shrink the table upon calling table.freeze? (Or possibly flag the table to be shrunk during GC?) Most use cases for table.freeze involve tables that stick around for a while in memory, so it might be worth it.
Mainly I’d like dictionaries and mixed tables to use less memory once they are set up, as there’s no way to pre-allocate the hash part of a table. Here’s an example:

local function foo(array: {string})
	local t = {}
	for i, v in ipairs(array) do
		t[v] = true
	end
	-- Could the table shrink here to use less memory?
	table.freeze(t)
	return t
end

local t = foo({"A", "B", "C", "D", "E"})

Arrays are easy to preallocate using table.create, but here’s an array example anyways:

local t = {}
-- This will resize 't' to 32.
for i = 1, 17 do
	t[i] = true
end
-- 't' could shrink back to 17 here and save 240 bytes of memory.
table.freeze(t)

The problem with this is that shrinking the table can rearrange the table, which may change the order of iteration.

local k1 = next(t)
local k2 = next(t)
print(rawequal(k1,k2)) -- must be true
table.freeze(t)
local k3 = next(t)
print(rawequal(k1,k3)) -- ?

This would break code that freezes a table while iterating over it. Even if seldom done it would be confusing that table.freeze can rearrange the table.

local t = {a=1,b=2,c=3,d=4}
for i,v in pairs(t) do
	if i == "a" then
		table.freeze(t)
	end
	print(i,v)
end
-- assume the initial order is d -> c -> b -> a
-- and because table.freeze shrunk the table it
-- changed the order to a -> b -> c -> d
-- this would cause b, c, and d get iterated over twice

Maybe another function named something like table.shrink would be better for that.

local t = {}
-- expands 't' to 32
for i=1,17 do
	t[i] = true
end
table.shrink(t) -- shrinks 't' to 17
table.freeze(t)

This function shouldn’t be allowed to shrink frozen tables because it can rearrange the table.

Shrinking the hash part is confusing in your example, would it shrink to the smallest size possible to save memory or to a reasonable size to reduce collisions but still save some memory? Shrinking the hash part after removing many elements could be helpful in some cases, because removing elements isn’t allowed to rearrange the table.

This sounds like a bad idea, because shrinking can rearrange the table

local t = setmetatable({},{__shrink=true}) -- let __shrink be this flag
t.a,t.b,t.c=1,2,3 -- add keys
for i,v in pairs(t) do
	print(i,v) -- this is now wrong!
	-- assume the initial order is a -> b -> c
	-- and the GC runs while iterating over b and it
	-- changes the order to b -> a -> c
	-- this would cause a to get printed out twice
	-- in theory this could loop forever
end

Allowing for the GC to shrink the table means that you can’t rely on a consistent order when no new keys are inserted, which practically means you would never iterate over it.

Having a way of preallocating the hash part would be better for this use case than allocating the hash part many times and shrinking it at the end.

This isn’t possible. Tables sort keys internally so they can be accessed efficiently. pairs / next iteration order is unspecified.

You might be right about this. I don’t think standard Lua would have any problems because the order would remain the same, but Luau’s pairs / next optimizations might need to account for it.

The only way you can really create a table with different pairs / next order is if you use the hash part instead of the array part for integer keys greater than 0.

Example code
local function test(name, t)
	print(name)
	for k in pairs(t) do
		print(k)
	end
end
-- I get (0, 1, -1) for the hash version and (1, 0, -1) for the mixed version.
test("Hash", {
	[-1] = true,
	[0] = true,
	[1] = true, -- use hash part
})
test("Mixed", {
	[-1] = true,
	[0] = true,
	true, -- use array part
})

I was initially hoping to be able to preallocate the hash part using table.create. Preallocating arrays is simple and can improve readability, but hash tables aren’t quite as straightforward. The VM needs to decide whether to use the hash part or the array part once you start using number keys, so it’s not a broad solution unless that behavior is specified and it’s possible to preallocate both.

That’s why I said “assume”, there isn’t anything wrong with the order so I chose it for the example. I’m just choosing 1 specific order to show what is problematic, I wasn’t stating that is the correct order. You can choose any other order and see that problems may arise.

There is no guarantee the order would remain the same, and rearranging the table may remove information about keys which have been removed so that passing them to next works fine.

local t = {1}
print(next(t,1)) --> nil
t[1] = nil
print(next(t,1)) --> nil
-- information about the key still exists, which makes this work
-- this allows loops like this:
-- for i in pairs(t) do t[i] = nil end
-- to work even though 
t.key = 1
print(next(t,1)) -- invalid key to 'next'
-- adding a new key is allowed to rearrange the table
-- and in this case it does
-- if table.freeze could rearrange the table then the same problem could happen