Luau Recap: September 2021

Welcome to the Luau Recap!

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

Generic functions

The big news this month is that generic functions are back!

Luau has always supported type inference for generic functions, for example:

type Point<X,Y> = { x: X, y: Y }
function swap(p)
  return { x = p.y, y = p.x }
end
local p : Point<number, string> = swap({ x = "hi", y = 37 })
local q : Point<boolean, string> = swap({ x = "hi", y = true })

but up until now, there’s been no way to write the type of swap , since Luau didn’t have type parameters to functions (just regular old data parameters). Well, now you can:

function swap<X, Y>(p : Point<X, Y>): Point<Y, X>
  return { x = p.y, y = p.x }
end

Generic functions can be used in function declarations, and function types too, for example

type Swapper = { swap : <X, Y>(Point<X, Y>) -> Point<Y, X> }

People may remember that back in April we announced generic functions, but then had to disable them. That was because DataBrain discovered a nasty interaction between typeof and generics, which meant that it was possible to write code that needed nested generic functions, which weren’t supported back then.

Well, now we do support nested generic functions, so you can write code like

function mkPoint(x)
  return function(y)
    return { x = x, y = y }
  end
end

and have Luau infer a type where a generic function returns a generic function

function mkPoint<X>(x : X) : <Y>(Y) -> Point<X,Y>
  return function<Y>(y : Y) : Point<X,Y>
    return { x = x, y = y }
  end
end

For people who like jargon, Luau now supports Rank N Types , where previously it only supported Rank 1 Types.

Bidirectional typechecking

Up until now, Luau has used bottom-up typechecking. For example, for a function call f(x) we first find the type of f (say it’s (T)->U ) and the type for x (say it’s V ), make sure that V is a subtype of T , so the type of f(x) is U .

This works in many cases, but has problems with examples like registering callback event handlers. In code like

part.Touched:Connect(function (other) ... end)

if we try to typecheck this bottom-up, we have a problem because we don’t know the type of other when we typecheck the body of the function.

What we want in this case is a mix of bottom-up and top-down typechecking. In this case, from the type of part.Touched:Connect we know that other must have type BasePart .

This mix of top-down and bottom-up typechecking is called bidirectional typechecking , and means that tools like type-directed autocomplete can provide better suggestions.

Editor features

We have made some improvements to the Luau-powered autocomplete beta feature in Roblox Studio:

  • We no longer give autocomplete suggestions for client-only APIs in server-side scripts, or vice versa.
  • For table literals with known shape, we provide autocomplete suggestions for properties.
  • We provide autocomplete suggestions for Player.PlayerGui .
  • Keywords such as then and else are autocompleted better.
  • Autocompletion is disabled inside a comment span (a comment starting --[[ ).

Typechecking improvements

In other typechecking news:

  • The Luau constraint resolver can now refine the operands of equality expressions.
  • Luau type guard refinements now support more arbitrary cases, for instance typeof(foo) ~= "Instance" eliminates anything not a subclass of Instance .
  • We fixed some crashes caused by use-after-free during type inference.
  • We do a better job of tracking updates when script is moved inside the data model.
  • We fixed one of the ways that recursive types could cause free types to leak.
  • We improved the way that return statements interact with mutually recursive function declarations.
  • We improved parser recovery from code which looks like a function call (but isn’t) such as
local x = y
(expr)[smth] = z
  • We consistently report parse errors before type errors.
  • We display more types as *unknown* rather than as an internal type name like error#### .
  • Luau now infers the result of Instance:Clone() much more accurately.

Performance improvements

  • Vector3.new constructor has been optimized and is now ~2x faster
  • A previously implemented optimization for table size prediction has been enhanced to predict final table size when setmetatable is used, such as local self = setmetatable({}, Klass)
  • Method calls for user-specified objects have been optimized and are now 2-4% faster
  • debug.traceback is now 1.5x faster, although debug.info is likely still superior for performance-conscious code
  • Creating table literals with explicit numeric indices, such as { [1] = 42 } , is now noticeably faster, although list-style construction is still recommended.

Other improvements

  • The existing ‘TableLiteral’ lint now flags cases when table literals have duplicate numeric indices, such as { [1] = 1, [1] = 2 }
84 Likes

This topic was automatically opened after 10 minutes.

This has been bothering me for god-knows-how long, I’m so glad it’s finally supported now! Great to see all the optimizations too - I found it surprising how much I care about micro-optimizations, when in reality they (usually) only matter in really performance-intensive code, lol.

8 Likes

This could be massive. This syntax used to put the index in the hash portion of the table which would add overhead. From my very limited testing this seems to slap these down in the array portion. The optimization is the least important part. next, at least in vanilla Lua, prioritizes the array portion of the table before moving to the hashmap to iterate, and it does it in order. Even though this behavior isn’t defined, this change makes it so tables created with explicit numeric indices can be iterated on in order with pairs and next.

more room for me to be lazy :woozy_face:

4 Likes

Yup, you are correct on both counts.

9 Likes

Thanks a lot for adding my feature request!

1 Like

WHAT
WHAT
WHAT

y’all had me screaming in class .
WE HAVE GENERIC FUNCTIONS AGAIN WOOOOOOHOOOOOOO :smiley:

THIS IS HUUUUUUGE
(p.s. thank you! <3)

4 Likes

Something I want to report, Luau’s script editor at the moment thinks having (...) -> () as a function definition is valid, which it isn’t, as you need to specify the type of the arguments (...any) -> ().

It throws out an error, but the editor doesn’t seem to think it is invalid code.

image
image

2 Likes

Did this improve Vector3 math as well, since it creates new Vector3s?

2 Likes

No, it was already using an optimal construction path, this only refers to explicit calls to Vector3.new

7 Likes

You love to see it.

Is there any updates on type-checking being taken into account by the code compiler and providing a performance boost for games? :flushed:

1 Like

Not yet, sorry!

This requires analyzing our type system for what’s called soundness - specifically, augmenting the type engine with extra information about when it’s basically 100% confident in the type vs where the type may be a lie, such as due to use of ::. Right now we’re fully focused on making sure that the type checking core is robust, helpful wrt inference results, and more unified between nonstrict and strict modes, as right now the type inference engine makes very different decisions between these two modes. Only after this work is done will we be able to start looking at soundness properties of our type system.

Additionally, the exact knowledge of types in practice can help us in two ways:

  • Advanced compiler optimizations, such as being able to reuse computations between different parts of the code when we know the exact types
  • Just in time compilation, where the extra overhead for checking the types, which is comparatively small in the interpreter, becomes comparatively large.

We aren’t doing these yet… but we plan to! So stay tuned but give us time, nothing in this realm will ship this year.

12 Likes

@zeuxcg @FunnyOldWorld

Hey, I got a sneak peak of this bidirectional type inference feature and found a major bug with it which I was unfortunately too busy to create a bug report about before this announcement. Hopefully this can be fixed easily without too much hassle? I love the bidirectional typing, and it’s easy to work around this bug by either annotating all parameters, or annotating none of them.

EDIT: See Bidirectional Typing gets borked when annotating some but not all parameters in a callback for a public bug report

1 Like

An optimization to closure allocation was released a few weeks back that affected closures without upvalues. Last week, it was further enhanced to closures with upvalues:

Is there any way we can know what the “complex microarchitectural conditions” are? This optimization, while effective, seems risky in some ways (mostly when combined with setfenv), and it would be nice to know what the conditions are to avoid running into issues.

2 Likes

This optimization is currently only active in Studio and waiting for mobile updates for us to fully test on live games. In the next recap we should know if it’s good enough to stay or not, and subsequently we’ll release more information. However it’s important to know that, if that optimization stays, we won’t promise that the conditions for when it activates are set in stone (they may change in the future).

The interaction with setfenv can be very roughly summarized as “if setfenv is used with a new table before the closure is created, the closure allocation will not be elided”.

4 Likes

I’ve replied in the bug report topic, but for visibility here:
We’re going to disable bidirectional type inference for function arguments until the fix is released.

4 Likes

Sorry if I sound dumb here,

This is something I’ve been wondering for a while,
Apparently Lua has some issues with specifying a number index on the table constructor, on which that index will be stored in the hash part instead, even if everything inside that table constructor is in order and shaped exactly like an array.

Is this optimization about that? If not, I was wondering if this was ever changed in Luau?

1 Like

Answer is yes, it has been answered in this very same post already:

2 Likes

Why doesn’t this function work?

function assertValue<T>(a: T?): T
	assert(a)
	return a
end

It should work.
Do you have Studio version 500?