Luau Recap: October 2021

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

[Cross-posted to the Roblox Developer Forum.]

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}?)
    local a = t and t.x or 5 -- 'a' is a 'number', no false positive errors here
end

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

--!strict
function f(t: {x: number}?)
    local a = if t then t.x else 5 -- 'a' is a 'number', no false positive errors here
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
    return function(a, b) return a > b end -- a and b are now known to be 'number' here
end

local comp = getSortFunction()

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

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}?)
    return a[1] -- "Value of type '{number}?' could be nil" instead of "'{number}?' is not a table'
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.

Updated: