Luau Recap: October 2023

We’re still quite busy working on some big type checking updates that we hope to talk about soon, but we have a few equally exciting updates to share in the meantime!

Let’s dive in!

Floor Division

Luau now has a floor division operator. It is spelled //:

local a = 10 // 3 -- a == 3
a //= 2           -- a == 1

For numbers, a // b is equivalent to math.floor(a / b), and you can also overload this operator by implementing the __idiv metamethod. The syntax and semantics are borrowed from Lua 5.3 (although Lua 5.3 has an integer type while we don’t, we tried to match the behavior to be as close as possible).

You can also use // with Vector3 values. Floor division can be performed with both scalars or for each component of the vector:

local a = Vector3.new(12, 10, 8.5)
local f = Vector3.new(3, 2, 1)

local b = a // 3 -- 4, 3, 2
local c = a // f -- 4, 5, 8

Native Codegen Preview

We are actively working on our new native code generation module that can significantly improve the performance of compute-dense scripts by compiling them to X64 (Intel/AMD) or A64 (ARM) machine code and executing that natively. We aim to support all AArch64 hardware with the current focus being Apple Silicon (M1-M3) chips and all Intel/AMD hardware that supports AVX1 (with no planned support for earlier systems). When the hardware does not support native code generation, any code that would be compiled as native just falls back to the interpreted execution.

When working with open-source releases, binaries now have native code generation support compiled in by default; you need to pass --codegen command line flag to enable it. If you use Luau as a library in a third-party application, you would need to manually link Luau.CodeGen library and call the necessary functions to compile specific modules as needed - or keep using the interpreter if you want to! If you work in Roblox Studio, we have integrated native code generation preview as a beta feature, which currently requires manual annotation of select scripts with --!native comment.

Our goal for the native code generation is to help reach ultimate performance for code that needs to process data very efficiently, but not necessarily to accelerate every line of code, and not to replace the interpreter. We remain committed to maximizing interpreted execution performance, as not all platforms will support native code generation, and it’s not always practical to use native code generation for large code bases because it has a larger memory impact than bytecode. We intend for this to unlock new performance opportunities for complex features and algorithms, e.g. code that spends a lot of time working with numbers and arrays, but not to dramatically change performance on UI code or code that spends a lot of its time calling Lua functions like table.sort, or external C functions (like Roblox engine APIs).

Importantly, native code generation does not change our behavior or correctness expectations. Code compiled natively should give the same results when it executes as non-native code (just take a little less time), and it should not result in any memory safety or sandboxing issues. If you ever notice native code giving a different result from non-native code, please submit a bug report.

We continue to work on many code size and performance improvements; here’s a summary of what we’ve done in the last couple of months, and there’s more to come!

  • Repeated access to table fields with the same object and name are now optimized (e.g. t.x = t.x + 5 is faster)

  • Numerical for loops are now compiled more efficiently, yielding significant speedups on hot loops

  • Bit operations with constants are now compiled more efficiently on X64 (for example, bit32.lshift(x, 1) is faster); this optimization was already in place for A64

  • Repeated access to array elements with the same object and index is now faster in certain cases

  • Performance of function calls has been marginally improved on X64 and A64

  • Fix code generation for some bit32.extract variants where we could produce incorrect results

  • table.insert is now faster when called with two arguments as it’s compiled directly to native code

  • To reduce code size, module code outside of functions is not compiled natively unless it has loops

Analysis Improvements

The break and continue keywords can now be used in loop bodies to refine variables. This was contributed by a community member - thank you, AmberGraceSoftware!

function f(objects: {{value: string?}})
	for _, object in objects do
    	if not object.value then
        	continue
    	end
   	 
    	local x: string = object.value -- ok!
	end
end

When type information is present, we will now emit a warning when # or ipairs is used on a table that has no numeric keys or indexers. This helps avoid common bugs like using #t == 0 to check if a dictionary is empty.

local message = { data = { 1, 2, 3 } }

if #message == 0 then -- Using '#' on a table without an array part is likely a bug
end

Finally, some uses of getfenv/setfenv are now flagged as deprecated. We do not plan to remove support for getfenv/setfenv but we actively discourage its use as it disables many optimizations throughout the compiler, runtime, and native code generation, and interferes with type checking and linting.

Autocomplete Improvements

We used to have a bug that would arise in the following situation:

--!strict
type Direction = "Left" | "Right"
local dir: Direction = "Left"

if dir == ""| then
end

(imagine the cursor is at the position of the | character in the if statement)

We used to suggest Left and Right even though they are not valid completions at that position. This is now fixed.

We’ve also added a complete suggestion for anonymous functions if one would be valid for the requested position. For example:

local p = Instance.new('Part')
p.Touched:Connect(

You will see a completion suggestion function (anonymous autofilled). Selecting that will cause the following to be inserted into your code:

local p = Instance.new('Part')
p.Touched:Connect(function(otherPart: BasePart)  end

We also fixed some confusing editor feedback in the following case:

game:FindFirstChild(

Previously, the signature help tooltip would erroneously tell you that you needed to pass a self argument. We now correctly offer the signature FindFirstChild(name: string, recursive: boolean?): Instance

Runtime Improvements

  • string.format’s handling of %* and %s is now 1.5-2x faster

  • tonumber and tostring are now 1.5x and 2.5x faster respectively when working on primitive types

  • Compiler now recognizes math.pi and math.huge and performs constant folding on the expressions that involve these at -O2; for example, math.pi*2 is now free.

  • Compiler now optimizes if...then...else expressions into AND/OR form when possible (for example, if x then x else y now compiles as x or y)

  • We had a few bugs around repeat..until statements when the until condition referred to local variables defined in the loop body. These bugs have been fixed.

  • Fix an oversight that could lead to string.char and string.sub generating potentially unlimited amounts of garbage and exhausting all available memory.

  • We had a bug that could cause the compiler to unroll loops that it really shouldn’t. This could result in massive bytecode bloat. It is now fixed.

luau-lang on GitHub

If you’ve been paying attention to our GitHub projects, you may have noticed that we’ve moved luau repository to a new luau-lang GitHub organization! This is purely an organizational change but it’s helping us split a few repositories for working with documentation and RFCs and be more organized with pull requests in different areas.

Make sure to update your bookmarks and star our main repository if you haven’t already!

Lastly, a big thanks to our open source community for their generous contributions!

211 Likes

This topic was automatically opened after 10 minutes.

Ok, wow, I did not expect a Floor Division to be part of the key operators in LuaU.
Not what expected, but a welcome change regardless.

37 Likes

Obligatory “where buffer” comment so nobody else has to ask where the buffer type is.

Excited to see integer division live now. It will remove almost every use of math.floor I have.

35 Likes

Fantastic changes, it’s always nice to see optimisation improvements.

17 Likes

This reminds me, we very much need a way to detect if the code in a certain position is running natively.
Maybe something like: debug.getnative():boolean
I’m not sure if this is possible, but something like this would be much more reliable than relying purely on performance differences.

An equivalent for parallel code would be nice, but it’s not as needed.

11 Likes

Why is this? It should be invisible to users.

16 Likes

Man, I simply halt everything when these lua recaps get announced. I just indulge in reading them, and I get excited because there are things in the announcements that I missed, so it seems like they were just announced.

10 Likes

finally i can get a hit of dopamine when i divide a number by 2

16 Likes

Really happy with all the new changes, specifically native code generation and floor divison!

Kinda wish that the upcoming native code generation release would include localscript support for any device that supports native code generation but I guess that’s not coming any time soon… For now only server script support, hope it’s extra optimized for running on Roblox’s servers in some way since they know what it’ll run on exactly beforehand.

8 Likes

Shouldn’t the return type be ‘Instance?’, since FindFirstChild isn’t guaranteed to return? I swear it was like that before this change.

10 Likes

It’s most likely a typo.

8 Likes

No more do I have to use math.floor() to get rid of those pesky floating point errors.

18 Likes

No it’s like that in studio too

7 Likes

A function is entirely unnecessary when you can approximate this using heuristics (read what you quoted).

Is the code a function? Yes: it’s probably native | No: no
Is the code a loop? Yes: it’s probably native | No: no

You should be fully aware of whether or not your code is running in parallel because you have to explicitly desynchronize threads.


Is this dependent on the typechecker mode?

8 Likes

It happens in strict mode (and probably nonstrict too because why would the type annotation change?)

6 Likes

There is no way to tell if somebody calls a module function through parallel without using a pcall.

4 Likes

It seems like this is not supported with Vector2 values, is this intentional or just an oversight?

This also appears to be the case with Vector3int16 and Vector2int16 values which support division / but not floor division //, though these types aren’t very important.

7 Likes

I just saw that it got synced to the upstream branch, which I assume means it’ll arrive soon?

4 Likes

This information will be shown in the Script Profiler. The behavior of native and non-native functions should be the same, so it’s not obvious why a debug function would be necessary.

11 Likes