Luau Recap: October 2021

Luau is our new language that you can read more about at https://luau-lang.org.

if-then-else expression

In addition to supporting standard if statements , Luau adds support for if expressions . Syntactically, if-then-else expressions look very similar to if statements. However instead of conditionally executing blocks of code, if expressions conditionally evaluate expressions and return the value produced as a result. Also, unlike if statements, if expressions do not terminate with the end keyword.

Here is a simple example of an if-then-else expression:

local maxValue = if a > b then a else b

if-then-else expressions may occur in any place a regular expression is used. The if-then-else expression must match if <expr> then <expr> else <expr> ; it can also contain an arbitrary number of elseif clauses, like if <expr> then <expr> elseif <expr> then <expr> else <expr> . Note that in either case, else is mandatory.

Here’s is an example demonstrating elseif :

local sign = if x < 0 then -1 elseif x > 0 then 1 else 0

Note: In Luau, the if-then-else expression is preferred vs the standard Lua idiom of writing a and b or c (which roughly simulates a ternary operator). However, the Lua idiom may return an unexpected result if b evaluates to false. The if-then-else expression will behave as expected in all situations.

Library improvements

New additions to the table library have arrived:

function table.freeze(t)

Given a non-frozen table, freezes it such that all subsequent attempts to modify the table or assign its metatable raise an error. If the input table is already frozen or has a protected metatable, the function raises an error; otherwise it returns the input table. Note that the table is frozen in-place and is not being copied. Additionally, only t is frozen, and keys/values/metatable of t don’t change their state and need to be frozen separately if desired.

function table.isfrozen(t): boolean

Returns true if and only if the input table is frozen.

Typechecking improvements

We continue work on our type constraint resolver and have multiple improvements this month.

We now resolve constraints that are created by or expressions. In the following example, by checking against multiple type alternatives, we learn that value is a union of those types:

--!strict
local function f(x: any)
    if type(x) == "number" or type(x) == "string" then
        local foo = x -- 'foo' type is known to be 'number | string' here
        -- ...
    end
end

Support for or constraints allowed us to handle additional scenarios with and and not expressions to reduce false positives after specific type guards.

And speaking of type guards, we now correctly handle sub-class relationships in those checks:

--!strict
local function f(x: Part | Folder | string)
    if typeof(x) == "Instance" then
        local foo = x -- 'foo' type is known to be 'Part | Folder' here
    else
        local bar = x -- 'bar' type is known to be 'string' here
    end
end

One more fix handles the a and b or c expression when ‘b’ depends on ‘a’:

--!strict
function f(t: {x: number}?)
    -- 'a' is a 'number', no false positive errors here
    local a = t and t.x or 5
end

Of course, our new if-then-else expressions handle this case as well.

--!strict
function f(t: {x: number}?)
    -- 'a' is a 'number', no false positive errors here
    local a = if t then t.x else 5
end

We have extended bidirectional typechecking that was announced last month to propagate types in additional statements and expressions.

--!strict
function getSortFunction(): (number, number) -> boolean
    -- a and b are now known to be 'number' here
    return function(a, b) return a > b end
end

local comp = getSortFunction()

-- a and b are now known to be 'number' here as well
comp = function(a, b) return a < b end

We’ve also improved some of our messages with union types and optional types (unions types with nil ).

When optional types are used incorrectly, you get better messages. For example:

--!strict
function f(a: {number}?)
    -- "Value of type '{number}?' could be nil"
    -- instead of "'{number}?' is not a table"
    return a[1]
end

When a property of a union type is accessed, but is missing from some of the options, we will report which options are not valid:

--!strict
type A = { x: number, y: number }
type B = { x: number }
local a: A | B
local b = a.y -- Key 'y' is missing from 'B' in the type 'A | B'

When we enabled generic functions last month, some users might have seen a strange error about generic functions not being compatible with regular ones.

This was caused by undefined behaviour of recursive types. We have now added a restriction on how generic type parameters can be used in recursive types: RFC: Recursive type restriction

Performance improvements

An improvement to the Stop-The-World (atomic in Lua terms) stage of the garbage collector was made to reduce time taken by that step by 4x factor. While this step only happens once during a GC cycle, it cannot be split into small parts and long times were visible as frame time spikes.

Table construction and resize was optimized further; as a result, many instances of table construction see 10-20% improvements for smaller tables on all platforms and 20%+ improvements on Windows.

Bytecode compiler has been optimized for giant table literals, resulting in 3x higher compilation throughput for certain files on AMD Zen architecture.

Coroutine resumption has been optimized and is now ~10% faster for coroutine-heavy code.

Array reads and writes are also now a bit faster resulting in 1-3% lift in array-heavy benchmarks.

155 Likes

This topic was automatically opened after 10 minutes.

This is great and I love the new if-then-else system a lot better than spamming and and parenthesis

Is there a single way to unfreeze a table, or if there will be any method in the future to do so?

But I would be excited for a const indentifier will come to luau like with lua 5.4

local thing <const>  = 'a'

thing = 'b' -- errors

Or if they will ever add a __close metamethod to use as an alternative to the deprecated __gc

13 Likes

In the case of writing a read-only table, would table.freeze() be preferred to writing a proxy table? Is there any worthwhile difference in performance? I’m assuming that because you said it doesn’t copy the table it would be more ideal.

Great to see all this work on table type performance, always welcome in the changelog.

4 Likes

IMO the ability to unfreeze a table would void the point of it. The point of freezing is to create immutability, and so the solution would be to copy the table and use that as the unfrozen version.

18 Likes

This is amazing!!! Easily one of the best updates.

The new performance updates for tables are great, especially as I have multiple projects with large tables storing information.

This is easily the most exciting era of Roblox, I can’t wait for the spike of ultra-fast and performant games to be made.

3 Likes

There’s no way to unfreeze the table; freezing is permanent.

We don’t have plans to add __close; if const locals ever get added it’ll be through const var = value syntax, not the Lua 5.4 <> syntax that we are unlikely to ever adopt.

11 Likes

Is there any performance benefit to freezing a table? or is it purely to provide immutability?
If there isn’t right now, is it possible that there will be in the future?

3 Likes

Interesting update.
A way more shorter to set a value with if statements.
Really exiting of what is coming next!

2 Likes

Are there any clear benefits of using if, then, and else over and and or? This is the only thing I can find:

I tend to use and and or on small to medium lengthed statements, and reserve if, else, and elseif for longer statements or blocks of code that require the creation of new variables:

local x: number = 1

print(x == 1 and "x is 1!" or "x is not 1!") -- I'd use this
print(if x == 1 then "x is 1!" else "x is not 1!") -- Rather than this

If I do it with if, else, and elseif it just feels improper as it basically functions like normal if, else, and elseif but on one line.

2 Likes

I guess if you want to return nil or false in some situations, using logical operators will break your code. For example:

local a = (b==c and nil) or (b>c and false) or (b<c and true)

5 Likes

The following practical code does not work as expected with the and/or pattern:

local myObject = nil
local myDefault = Instance.new(...)
local condition = true
print(condition and myObject or myDefault) --> myDefault... ??

It turns out that this situation is extremely easy to run into in practice, and has been the cause of a lot of bugs in our Lua code, especially related to fast flagging since fast flagging often involves branching in table creation expressions etc which was most easily done with and/or.

5 Likes

There’s currently no performance benefit to freezing. We are considering an optimization for metatable lookup where frozen metatables will be faster in OOP scenarios but this hasn’t been designed yet so for now I would not recommend making performance-related assumptions here.

7 Likes

Another similar example for this type of error is when the “true” alternative is a boolean with the value false, e.g.

local fooHidden = foo and foo.hidden or true

always evaluates to true, although false may be intended when foo.hidden == false.

2 Likes

Wouldn’t you use math.min for that?

2 Likes

There is a problem with indentation and end autocompletion when using if expressions:

x

16 Likes

Luau (+ studio autocomplete!) is my favorite feature of roblox.
Keep it up!

2 Likes

__close would be better as __destroy for Luau.

1 Like

As a temporary workaround, you can wrap the expressions in parentheses.

4 Likes

Will the new Luau be forced?

I dont wanna learn an entirely new coding engine after I just finished learning Lua.

1 Like