Luau Recap: May 2022

This month Luau team has worked to bring you a new language feature together with more type checking improvements and bug fixes!

Generalized iteration

We have extended the semantics of standard Lua syntax for iterating through containers, for vars in values with support for generalized iteration.
In Lua, to iterate over a table you need to use an iterator like next or a function that returns one like pairs or ipairs. In Luau, you can now simply iterate over a table:

for k, v in {1, 4, 9} do
    assert(k * k == v)
end

This works for tables but can also be customized for tables or userdata by implementing __iter metamethod. It is called before the iteration begins, and should return an iterator function like next (or a custom one):

local obj = { items = {1, 4, 9} }
setmetatable(obj, { __iter = function(o) return next, o.items end })

for k, v in obj do
    assert(k * k == v)
end

The default iteration order for tables is specified to be consecutive for elements 1..#t and unordered after that, visiting every element.
Similar to iteration using pairs , modifying the table entries for keys other than the current one results in unspecified behavior.

Typechecking improvements

We have added a missing check to compare implicit table keys against the key type of the table indexer:

-- error is correctly reported, implicit keys (1,2,3) are not compatible with [string] 
local t: { [string]: boolean } = { true, true, false }

Rules for == and ~= have been relaxed for union types, if any of the union parts can be compared, operation succeeds:

--!strict
local function compare(v1: Vector3, v2: Vector3?)
    return v1 == v2 -- no longer an error
end

Table value type propagation now correctly works with [any] key type:

--!strict
type X = {[any]: string | boolean}
local x: X = { key = "str" } -- no longer gives an incorrect error

If a generic function doesn’t provide type annotations for all arguments and the return value, additional generic type parameters might be added automatically:

-- previously it was foo<T>, now it's foo<T, b>, because the second argument is also generic
function foo<T>(x: T, y) end

We have also fixed various issues that have caused crashes, with many of them coming from your bug reports.

Autocomplete improvements

Studio autocomplete has received a few improvements:

  • Function argument names have been added to the function signature display
  • Instance:GetAttribute and Instance:SetAttribute will now suggest attribute names if instance can be resolved
  • Inside the new if-then-else expression, incorrect suggestions following else have been fixed
  • Deprecated lowercase Color3:toHSV so it’s no longer suggested
  • Deprecated lowercase Instance:findFirstChild , Instance:clone , Instance:remove , Instance:getChildren and old alias called Instance:children .

Linter improvements

GlobalUsedAsLocal lint warning has been extended to notice when global variable writes always happen before their use in a local scope, suggesting that they can be replaced with a local variable:

function bar()
    foo = 6 -- Global 'foo' is never read before being written. Consider changing it to local
    return foo
end
function baz()
    foo = 10
    return foo
end

Performance improvements

Garbage collection CPU utilization has been tuned to further reduce frame time spikes of individual collection steps and to bring different GC stages to the same level of CPU utilization.

Returning a type-cast local ( return a :: type ) as well as returning multiple local variables ( return a, b, c ) is now a little bit more efficient.

Function inlining and loop unrolling

In the open-source release of Luau, when optimization level 2 is enabled, compiler will now perform function inlining and loop unrolling.

Only loops with loop bounds known at compile time, such as for i=1,4 do , can be unrolled. The loop body must be simple enough for the optimization to be profitable; compiler uses heuristics to estimate the performance benefit and automatically decide if unrolling should be performed.

Only local functions (defined either as local function foo or local foo = function ) can be inlined. The function body must be simple enough for the optimization to be profitable; compiler uses heuristics to estimate the performance benefit and automatically decide if each call to the function should be inlined instead. Additionally, recursive invocations of a function can’t be inlined at this time, and inlining is completely disabled for modules that use getfenv / setfenv functions.

This optimization should be enabled in live Roblox experiences soon™.

119 Likes

This topic was automatically opened after 10 minutes.

It looks like the safe navigation operator is unfortunately shelved.

What I’d really like is a dedicated operator for accessing an object’s child, returning an arbitrary child with that name. parent[child] can return a property with the name of child, and this data could flow downstream when it would be desirable for an immediate error. parent:FindFirstChild(child) is unnecessarily verbose, has an unnecessary sense of order, and will not error if the child does not exist.

8 Likes

I quite like the suggestion of simply replacing the __index metamethod of instances, so that they instead return nil, rather than erroring. To be perfectly honest, the fact that indexing nil members of Instances ever errored in the first place, has always been a bit of an annoyance to me, as it’s inconsistent with indexing tables.

I don’t imagine the change would affect much anyway (beyond extremely hackish code; skimming error messages w/ pcall, or terminating threads with a deliberate error). The proposal of Luau caching identical functions was far more horrifying, and yet that shipped without issue. Meanwhile, I’d wager this change has a far greater demand, and yet we’re not even humoring its feasibility…

8 Likes

You can’t cut off all errors though. Some errors such as those due to security levels would definitely have to remain which is were things get more murky on whether that’s a good solution.

Personally I’m partial to the solution of __index taking an additional argument.

9 Likes

I love all the fantastic updates that always come each month with Luau. Reading these recaps for the month is like opening a present on Christmas. Thanks for all the hard work to make Luau a truly fantastic language to work with.
Specifically, being able to iterate over objects and tables directly is such a convenient thing to have! So long pairs() and ipairs() (for now)!
Can’t wait to see what we will get next month!!! :roblox: :roblox_light:

6 Likes

I love how Lua is becoming more like JavaScript day-by-day.

7 Likes

Would it be possible to add support for the __len metamethod for tables now? I been using the newproxy() for the __len support and it be nice to have that for the tables so I can use both the __iter and __len

5 Likes

Love the improvement to garbage collection! In the past I’ve struggled a lot with maintaining smooth performance, since GC would randomly decide it wanted to do a bunch of work on one frame and cause a nasty stutter.

3 Likes

Is there any documentation for this? I’d like to read up on it.

Also, I see there are improvements to garbage collection, any chance we could see an implementation of the __gc metamethod? :eyes::eyes::eyes:

Edit: nvm :frowning:

3 Likes

Nice update :+1:

Are you adding a way to narrow types by returning from a type check?

local function t(player: Player)
	local character = player.Character -- Model?
	
	if character == nil then return end
	
    -- Still thinks character could be nil	
	local humanoid = character:WaitForChild("Humanoid") 
end
5 Likes

Yes, we are going to support that.

6 Likes

as a workaround for now, you can add assert(character) right after the if-statement.

2 Likes

Hopefully the example in the recap is sufficient (also here Syntax - Luau); if you want a very thorough description you can read the full RFC (https://github.com/Roblox/luau/blob/master/rfcs/generalized-iteration.md)

3 Likes

Yeah we’ve discussed this as a potential next step, as with __iter this is the only metamethod you’d need to fully implement an indexed container. We’d need to be careful wrt potential performance impact, but we plan to look into this at least.

5 Likes

:thinking: yeah not a bad idea, although a bit ugly

I recall something about constant-type type-checking (e.g. strings, numbers, booleans, etc):

type constantStringABC = "ABC" -- Type matching only the string "ABC"

Is this still a planned feature? I find a lot of times I want to use this, particularly for enum-like types, like tables containing different parameters based on a particular key’s value which could be a set of constants.

(Type enums would actually be cool but that’s a different topic)

I really don’t like “generalized iteration”. I’m going to continue using pairs and ipairs explicitly. Please don’t ever deprecate them.

I think that’s called singleton types and IIRC that’s already live?

5 Likes

Indeed it is, I’m surprised I missed this.

I like this quite a lot, but I agree with you otherwise.

It is proper to use pairs when you expect/desire a table (and ipairs doesn’t get replaced by this at all since it is fundamentally different than pairs). If your use case is actually just iteration on the other hand (not usage of a table), this feature might be what you want, and, there’s been a few times I really wish I could’ve used this.

1 Like

While it’s up to you whether to continue using pairs/ipairs, there’s not really a benefit to using pairs or ipairs explicitly. I don’t think the word “proper” is, well, proper in this case :slight_smile: We specifically made sure that they don’t need to be used when iterating over tables or array-like tables.

That said obviously we don’t have plans to deprecate pairs/ipairs, so if people prefer them stylistically for some reason it’s fine to continue to use them.

1 Like