(yeah, I know, it’s technically April)
As a reminder, Luau (lowercase u, “l-wow”) is an umbrella initiative to improve our language stack - the syntax, compiler, virtual machine, builtin Lua libraries, type checker, linter (known as Script Analysis in Studio), and more related components. We continuously develop the language and runtime to improve performance, robustness and quality of life. Here we will talk about all things that happened since last update, which was just a month ago!
A lot of people work on these improvements; thanks @Apakovtac, @EthicalRobot, @fun_enthusiast, @zeuxcg! if you aren’t going to pat yourself on the back who else will?
If you have missed previous large announcements, here they are!
Let’s dive in.
New pcall/xpcall implementation
When the new VM was developed last year, we spent a lot of time profiling various scripts. In one of them, which was a benchmark of some simple tree manipulation in Roact, we were surprised to see pcall
taking a pretty significant amount of time - replacing it with xpcall
yielded large improvements in the benchmark throughput. The overhead comes from pcall
being able to handle yields, which was grafted on top of the existing functionality the VM provides.
xpcall
is fine and all, but many people don’t use it, and it would be awkward to recommend to use xpcall
when you need more performance, and you’re sure the code inside it doesn’t yield. Additionally, because xpcall
doesn’t support yielding, you can not debug the code running inside it.
To solve these problems, we rewrote the part of the VM that deals with coroutine resumption to support yielding across (some) C calls. This is supported by Lua 5.2; our implementation is somewhat different, and currently more constrained - for now we only support yields in pcall
/xpcall
- but more performant.
As a result:
-
pcall
is now much faster - up to 30x for simple functions! The performance now matches that ofxpcall
- Inside
pcall
, callingdebug.traceback
will return the full stack including the callers; similarly, when stepping intopcall
you’re going to see a full call stack in the debugger - If an error is generated inside
pcall
after the thread yields, we no longer print it to the output - this was a long standing issue that is fixed as a byproduct of this change. -
xpcall
now supports yielding (error function can’t yield but the main function can) -
xpcall
can now be debugged (step into & breakpoints work)
Please note that this change is not fully live on all client devices - it will take a few weeks for this change to propagate to mobile including older versions. It’s however live on desktop client, Studio and on the servers.
New debugger backend
The original debugger backend was written many years ago, and it relied on a VM mechanism called “hooks”. Briefly, in Studio when debugger was enabled in settings, every time the VM executed a line of code it called a C hook that had to check if the line had breakpoints set on it, or if it needed to step through the code.
This made an already not-very-fast VM much slower, and meant that the performance measurements you do while running scripts in Studio aren’t really representative. The old backend also had to rely on somewhat involved logic to filter out various debugger steps, and these complex interactions weren’t tested very well either.
We didn’t want to accept this state of affairs for the new VM and as such wrote a new debugger backend. This doesn’t impact the debugger UI - there’s a separate team working on improving that and the overall debugging experience! - but this does impact the low level debugging engine.
The new backend is more robust and doesn’t slow down script execution unless you’re actively stepping through the code. It works with the new VM (and only with the new VM), supports new pcall/xpcall and is thoroughly unit tested. We don’t expect any behavior regressions - there are a few slight differences around stepping, and some corner cases that the new backend handles better, but that’s about it.
Next week we’re going to ship a small improvement to the backend that will allow you to step over non-yieldable code (which old debugger couldn’t do either), for example, in this code:
local Class = {}
Class.__index = function(t, k) return rawget(Class, k) end
function Class:method()
print('method')
end
local obj = setmetatable({val = 42}, Class)
obj:method()
Stepping into obj:method()
breaks the script right now, but will work next week, bypassing the __index
call and jumping straight into the method body.
New VM is 100% live
Because the new debugger wasn’t fully functional (it took us time and a few tries to get it right), we had to maintain two VMs - one for Studio test sessions, and one for everything else. Up until this week, you still used the vanilla Lua VM in Studio Play Solo for this reason. Well, that’s not the case anymore!
With the new debugger backend active, we enabled the new Luau VM in every single context in Studio where it previously wasn’t running.
This means that every user on the platform is now running the Lua code with consistent performance, and has consistent access to all features like continue
or yieldable xpcall
. It unlocks some further internal optimizations that were just too painful to do in a dual-VM world, and in general makes further progress on language features and performance easier to make.
Old VM has served us well for 15 years, but it’s time to say goodbye.
Type annotation syntax - upcoming changes
We’re getting closer to finalizing the syntax of type annotations. We’ve looked at the remaining issues and external/internal feedback and decided to make a change to the syntax as follows:
- For function definitions, instead of using a “fat arrow” (
=>
), we now use a colon (:
) to delimit the return type:
function foo(a: number, b: number): number
return a + b
end
- For function types, instead of using a “fat arrow” (
=>
), we now use a “thin arrow” (->
) to delimit the return type:
type FooFunction = (number, number) -> number
This makes our syntax more consistent with the research project “Typed Lua” that was done by the university that develops Lua, as well as making us more consistent with some modern languages and makes it possible in theory for us to introduce clean shorthand lambda syntax later (not saying we will do this, but we wanted to have this option). Additionally, slim arrows are easier to read in type context like above since =
cleanly separates the type alias from the type definition.
This change will happen next week; we are going to support the old syntax for a bit, but it will be removed in a month or so. After we introduce the new syntax, we will be ready to promise syntax compatibility - meaning, it would be safe to upload code with type annotations to production and have it work in the future. NOTE this has not happened yet! Because of the syntax change, existing code with fat arrows will not be supported long term.
Type checking improvements
The type checker is still in beta and it’s seeing continuous improvements. We’re looking at various code bases in both strict and non-strict mode and resolving issues that come up.
As part of this, the type checker is now handling recursive function calls and complex data flow much better than it used to, which should eliminate most cases where in non-strict mode the type of a function in the same script can’t be inferred correctly.
Additionally setmetatable
didn’t correctly infer types in some cases and that was fixed as well.
Types can now work across require statements
Type aliases declared in modules are automatically exported and available, namespaced under the name of the local used to require:
local M = {}
type Sandwich = { slices: number }
function M.MakeSandwich()
return { slices = 5 }
end
return M
local Foo = require(script.Bar)
local test: Foo.Sandwich = Foo.MakeSandwich()
Please note that we have some bugs and limitations around require paths right now, especially around paths that start from game
- bear with us as we improve this over the coming weeks!