Luau Recap: June 2021

It’s July already?!

Last month, most of our team was busy working on improving Luau interaction with Roblox Studio for an upcoming feature this month, but we also managed to add typechecking and a few performance improvements as well!

Constraint Resolver

To improve type inference under conditional expressions and other dynamic type changes (like assignments) we have introduced a new constraint resolver framework into Luau type checker.

This framework allows us to handle more complex expressions that combine and / not operators and type guards.

Type guards support include expressions like:

  • if instance:IsA("ClassName") then
  • if enum:IsA("EnumName") then
  • if type(v) == "string" then

This framework is extensible and we have plans for future improvements with a == b / a ~= b equality constraints and handling of table field assignments.

It is now also possible to get better type information inside else blocks of an if statement.

A few examples to see the constraint resolver in action:

function say_hello(name: string?)
    -- extra parentheses were enough to trip the old typechecker
    if (name) then 
        print("Hello " .. name .. "!")
    else
        print("Hello mysterious stranger!")
    end
end
function say_hello(name: string?, surname: string?)
    -- but now we handle that and more complex expressions as well
    if not (name and surname) then
        print("Hello mysterious stranger!")
    else
        print("Hello " .. name .. " " .. surname .. "!")
    end
end

Please note that constraints are currently placed only on local and global variables.
One of our goals is to include support for table members in the future.

Typechecking improvements

We have improved the way we handled module require calls. Previously, we had a simple pattern match on the local m = require(...) statement, but now we have replaced it with a general handling of the function call in any context.

Handling of union types in equality operators was fixed to remove incorrect error reports.

A new IsA method was introduced to EnumItem to check the type of a Roblox Enum. This is intended to replace the enumItem.EnumType == Enum.NormalId pattern in the code for a construct that allows our constraint resolver to infer better types.

Additional fixes include:

  • table.pack return type was fixed
  • A limit was added for deeply nested code blocks to avoid a crash
  • We have improved the type names that are presented in error messages and Roblox Studio
  • Error recovery was added to field access of a table? type. While you add a check for nil , typechecking can continue with better type information in other expressions.
  • We handled a few internal compiler errors and rare crashes

Editor features

If you have Luau-Powered Type Hover beta feature enabled in Roblox Studio, you will see more function argument names inside function type hovers.

Behavior changes

We no longer allow referencing a function by name inside argument list of that function:

local function f(a: number, b: typeof(f)) -- 'f' is no longer visible here

Performance improvements

As always, we look for ways to improve performance of your scripts:

  • We have fixed memory use of Roblox Actor scripts in Parallel Luau beta feature
  • Performance of table clone through table.move has been greatly improved
  • Table length lookup has been optimized, which also brings improvement to table element insertion speed
  • Built-in Vector3 type support that we mentioned in April is now enabled for everyone
106 Likes

Performance updates are always great.

Are there any updates on generic functions? I don’t believe we have gotten one since they were removed.

4 Likes

Table length lookup has been optimized

Any details on this? You mention ‘lookup’, so is this achieved through interning?

2 Likes

This is cool! I’m still new to coding, so I probably won’t know how useful this will be until I’m advanced. :sweat_smile:

2 Likes

This is amazing, performance updates are great. Very cool, and thank you for this!

This was achieved by micro-optimizing the existing code without changing how length is stored. We do want to explore implementation with a cached value, but that’s not that simple.

In base Lua 5.1/5.2/5.3 implementation, length execution time depends on the length of the table, so the boost is not the same everywhere. But as an example, in a benchmark of inserting table elements in a loop we saw an improvement of 25-40%.

6 Likes

Awesome to see the constraint resolver improving with better type narrowing support!
It seems that the types for :FindFirstChild()/:FindFirstChildWhichIsA() etc. have changed incorrectly:

Notice how it resolves to Instance and not Instance?

Also, is it planned for :FindFirstChildWhichIsA("type") to be able to resolve to type? rather than Instance?. It would mean that you wouldn’t have to have a foo:IsA("type") check afterwards.

3 Likes

Your point stands, but note that you can already do this more ergonomically with:
local value = parent:FindFirstChildWhichIsA("Thing") :: Thing

2 Likes

That’s definitely true, but I assumed this would be more of an antipattern. I thought type assertion should be more of an “escape hatch” rather than used frequently.

Anyways, this isn’t a particularly important issue, just would be a nice to have :slight_smile:

It is, but if you have to choose between adding actual unnecessary code that has to execute at run-time and adding type annotations that shouldn’t be needed in the long run then you should probably pick the type annotations.

Because the code is also problematic in that it suggests that a condition exists when it actually doesn’t.

2 Likes

is that the roblox script update we got yesterday?
the roblox update 07.07.2021 Destroyed some important stuff in my game on the release day -_-

What kind of issues do you have in the recent update?

2 Likes

Are you considering adding Classes to Luau?

Having to use __index and setmetatable is confusing nomatter how many times I do it.

I :heart: this. This will make my life so much easier. THank you for finally adding a useful Luau update!

1 Like

Sorry to reply 4 days later, but this would definitely be interesting… Although I’m not sure how I feel about adding “Class” syntactics yet, or at all… cough C++ is a train wreck cough

Late reply, but I generally use a Class class that hides most of that.

return {
	Extend = function(self, o)
		o = o or {}
		self.__index = self
		setmetatable(o, self)
		return o
	end,
	
	-- Super = function(self, ...)
		-- return self:Extend(getmetatable(self).new(...))
	-- end,
	
	-- Bind = function(self, name) -- bind method by name
		-- return function(...)
			-- return self[name](self, ...)
		-- end
	-- end,
}

(The commented-out stuff isn’t 100% useful)

Usage example from an abandoned project:

local PhysicalPlate = Class:Extend()
-- you can set PhysicalPlate.foo for static foo

function PhysicalPlate.new()
	local model = script.Plate:Clone()
	
	-- extend the class to get an instance, then set instance properties
	return PhysicalPlate:Extend{
		Model = model,
		Joints = model.Joints,
		Anchor = model.Anchor,
		SizeTweening = Signal.new(),
	}
end

-- static method looks like this
function PhysicalPlate:Destroy()
	if self.Tween then self.Tween:Cancel() end
	self.Model:Destroy()
end

Apologies if this isn’t the best place for this, but…

This section seems like a bit of a stub. It offers an example, says it won’t work, and appears to offer a workaround… that doesn’t work. Is there an existing way to implement this idiom with --!strict on? If not, are there plans to do so?

I know you can just not assign a type to your instances, but it’d be nice if this was treated as a first-class concept given its wide usage.