Luau Recap: February 2022

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

Default type alias type parameters

We have introduced a syntax to provide default type arguments inside the type alias type parameter list.

It is now possible to have type functions where the instantiation can omit some type arguments.

You can provide concrete types:

--!strict 
type FieldResolver<T, Data = {[string]: any}> = (T, Data) -> number 
local a: FieldResolver<number> = ... 
local b: FieldResolver<number, {name: string}> = ...

Or reference parameters defined earlier in the list:

--!strict 
type EqComp<T, U = T> = (l: T, r: U) -> boolean 
local a: EqComp<number> = ... -- (l: number, r: number) -> boolean 
local b: EqComp<number, string> = ... -- (l: number, r: string) -> boolean

Type pack parameters can also have a default type pack:

--!strict 
type Process<T, U... = ...string> = (T) -> U... 
local a: Process<number> = ... -- (number) -> ...string 
local b: Process<number, (boolean, string)> = ... -- (number) -> (boolean, string)

If all type parameters have a default type, it is now possible to reference that without providing any type arguments:

--!strict 
type All<T = string, U = number> = (T) -> U 
local a: All -- ok local b: All<> -- ok as well

For more details, you can read the original RFC proposal.

Typechecking improvements

This month we had many fixes to improve our type inference and reduce false positive errors.

if-then-else expression can now have different types in each branch:

--!strict 
local a = if x then 5 else nil -- 'a' will have type 'number?' 
local b = if x then 1 else '2' -- 'b' will have type 'number | string'

And if the expected result type is known, you will not get an error in cases like these:

--!strict 
type T = {number | string} 
-- different array element types don't give an error if that is expected 
local c: T = if x then {1, "x", 2, "y"} else {0}

assert result is now known to not be ‘falsy’ ( false or nil ):

--!strict 
local function f(x: number?): number 
       return assert(x) -- no longer an error 
end

We fixed cases where length operator # reported an error when used on a compatible type:

--!strict 
local union: {number} | {string} 
local a = #union -- no longer an error

Functions with different variadic argument/return types are no longer compatible:

--!strict 
local function f(): (number, ...string) 
        return 2, "a", "b" 
end 

local g: () -> (number, ...boolean) = f -- error

We have also fixed:

  • false positive errors caused by incorrect reuse of generic types across different function declarations
  • issues with forward-declared intersection types
  • wrong return type annotation for table.move
  • various crashes reported by developers

Linter improvements

A new static analysis warning was introduced to mark incorrect use of a ’ a and b or c ’ pattern. When ‘b’ is ‘falsy’ ( false or nil ), result will always be ‘c’, even if the expression ‘a’ was true:

local function f(x: number) 
      -- The and-or expression always evaluates to the second alternative 
      --because the first alternative is false; consider using if-then-else expression instead 
      return x < 0.5 and false or 42 
end

Like we say in the warning, new if-then-else expression doesn’t have this pitfall:

local function g(x: number) 
      return if x < 0.5 then false else 42 
end

We have also introduced a check for misspelled comment directives:

--!non-strict 
-- ^ Unknown comment directive 'non-strict'; did you mean 'nonstrict'?

Performance improvements

For performance, we have changed how our Garbage Collector collects unreachable memory.
This rework makes it possible to free memory 2.5x faster and also comes with a small change to how we store Luau objects in memory. For example, each table now uses 16 fewer bytes on 64-bit platforms.

Another optimization was made for select(_, ...) call.
It is now using a special fast path that has constant-time complexity in number of arguments (~3x faster with 10 arguments).

Thanks

A special thanks to all the fine folks who contributed PRs this month!

76 Likes

This topic was automatically opened after 10 minutes.

Its really nice to see you guys polishing off these features you have been hard at work on for quite a while now.

I did notice those false positive cases in the past and appreciate the rectification of that!

As LuaU gets more refined by the amazing team that manages it I hope to see more people using it; not just on Roblox, but off platform aswell.

1 Like

Luau has recently had a typechecking regression and I’m disappointed that it has not been solved yet as it causes false positives in existing games. It may be linked to these changes.

--!strict

local C = {}
C.__index = C

function C.new(): C
	local self = setmetatable({}, C)
	self:update()
	return self
end

export type C = typeof(C.new())

function C.update(self: C) end

self:update() is flagged as invalid even though it used to be accepted.

3 Likes

Hey, I think the recent changes to how the GC collects unreachable memory has drastically affected a game I work on. Since this change the game has been suffering very large ping spikes every few seconds, in accordance with this one of the script rates has been seen jumping over 50,000 and higher. We’re also now seeing upwards of 2 GB of untracked memory on the server.

Game Link: Blacksite Zeta - Roblox

3 Likes

Thanks for the update. Learning something new every day.

I have one, very small, feature request for the type checker when displaying the argument types for methods. For example, when using OOP, if I have declared the function type for the method and call it using a colon :, it doesn’t automatically assume the self argument to be fulfilled and still displays it in the argument types, even though it’s already been passed. This can be annoying because it highlights an argument that has already been passed (the self argument) instead of the one is being passed.
Example:


Even though example should already register as being passed to the method, it’s saying that I’m passing the Vector3 to the self parameter. This isn’t a problem when using default datatypes like CFrame or Vector3
Example 2:
Screen Shot 2022-03-04 at 9.12.05 AM
Vs.

As shown, the default datatypes already take the colon into consideration when calling a function, I just want this same functionality for custom objects using OOP.

Of course this doesn’t effect me since I’m the one who wrote the function and I know what arguments need to be passed to it, but it may cause confusion to others when I decide to release the module I’m making. I don’t want to be hounded with questions about what they need to pass to the self argument when using the module.

There is one false positive I want to mention. When using brackets to index a property, a warning appears saying that it expected to be indexing a table, not the datatype being used.

Screen Shot 2022-03-04 at 9.25.29 AM
Vs.
Screen Shot 2022-03-04 at 9.23.36 AM

2 Likes

i love how roblox is improving these things, even though its a monthly recap, its still enjoyable to see these things unfold!

also, i’m curious on what this does, could someone explain with simpler programming analogies? thank you.

1 Like

That’s because Luau doesn’t yet support constant types (i.e. the type "X" rather than string). Once it does, this will likely get much better.

Can you please explain, in-depth, what constant types are? I vaguely understand what you mean about "type X instead of string", but I don’t fully grasp the significance of it yet.

A game I work on is experiencing very similar problems as those you described.

Has anyone reached out to you to provide assistance? Our player count has dropped drastically because of this and we need an immediate solution.

1 Like

Unfortunately nobody has reached out to me yet and we’re still experiencing the problem. I’m pretty sure these changes are defo what has caused the issue for us because even when the game has been reverted to days ago (when it was working fine) the issue is still there.

2 Likes

Luau already supports singleton types for strings and booleans though:

type Data = {
	isNumber: number,
	isString: string
}

for key, value in pairs(data) do
	-- key is type `'isNumber' | 'isString'`, rather than just `string`
	-- value is type `number | string`
end

local vec = Vector3.new(5, 6, 7)

for _, key in ipairs({'X', 'Y', 'Z'}) do
	-- key is type `'X' | 'Y' | 'Z'`
	-- indexing into Vector3 works:
	print(vec[key])
	-- Luau even knows what type it returns (number)
end

local propertyName: 'Changed' = 'Changed' -- has type `string` by default
local part = Instance.new('Part')

part[propertyName]:Connect(function() --[[ ... ]] end)

type UnionA = {
	variant: 'A',
	aProperty: boolean
}

type UnionB = {
	variant: 'B',
	bProperty: number
}

type UnionC = {
	variant: 'C',
	cProperty: Instance
}

type Union = UnionA | UnionB | UnionC

if union.variant == 'A' then
	-- Luau knows that only UnionA possibly has the variant 'A'
	-- This is the DiscriminableUnions FFlag.
	-- Luau will autocomplete for `aProperty` and infer its type correctly (no errors)
elseif union.variant == 'B' then
	-- Same for UnionB, etc.
end

This also allows Luau to support tuples (arrays with multiple different types):

local tuple = {1, true, game, {}}
-- tuple has type {number, boolean, DataModel, {}}
-- or: {[1]: number, [2]: boolean, [3]: DataModel, [4]: {}}
-- notice how the keys are specifically 1-4 rather than just `number`

But most of these features you won’t be able to use properly without inference because of the fact that Luau does not support many of the features of other type systems such as TypeScript (which is the gold standard of adding typing to an existing high-level scripting language). Also, I’m just shocked at how badly variadic types and multiple return values is implemented, quite possibly the worst of any first-class language feature.

Anyway - you can’t make, for example, a constructor function that specifically takes a ClassName and a table of properties to set on a created Instance, because of language limitations. I don’t think that will ever change.

1 Like

I am having similar issues with my game, dropped my player count in half!! Pings randomly spike up to 1.5k

https://www.roblox.com/games/1923555882/East-Brickton-SNOW

1 Like

This is me again tapping in to express my issues, I am desperate for help and this is literally killing my game and revenue! Please help!

1 Like

Why is roblox adding this? This is causing alot of issue into the roblox develovers like " Marcus760 " and others.

I have noticed that this raises a syntax error when used in function signatures. For example, this will raise a syntax error:

function Method<T = Instance>(value: T): ()
end

To get around this, I need to use:

type _MethodSignature<T = Instance> = (value: T) -> ()
local Method = (function(value)

end)::_MethodSignature

Edit: This seems to be a result of my own misconception, I assumed that the = operator was akin to the “extends” keyword in Java or “where” in C# i.e. the equivalent C# method would be

public void method<T>(T obj) where T : Instance {
}

Default type parameter values are not supported in function generic type list.
The main reason for that right now is that there is no way to instantiate generic functions with type parameters explicitly.

The feature you mention is what we call ‘constrained generics’ internally and while we have interest in addressing those in the future, they are not placed on the roadmap yet.

-- not supported yet, but you can imagine something like this:
type Signature1<T: Instance> = ... -- 'T' is at least an Instance
type Signature2<T: Instance = Part> = ... -- 'T' is at least an Instance and a Part by default
1 Like

I’m haivng an issue with ModuleScripts that return just a table are being marked as sealed.
This is then being shown as a warning when the requiring script attempts to add a property.

ModuleScript:
image

Script:
image

This didn’t used to be the case but now its become very annoying as it marks the whole function with a warning that I can’t seem to ignore even with --!nolint.