Luau Recap: July & August 2022

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

Tables now support __len metamethod

See the RFC Support __len metamethod for tables and rawlen function for more details.

With generalized iteration released in May, custom containers are easier than ever to use. The only thing missing was the fact that tables didn’t respect __len.

Simply, tables now honor the __len metamethod, and rawlen is also added with similar semantics as rawget and rawset:

local my_cool_container = setmetatable({ items = { 1, 2 } }, {
    __len = function(self) return #self.items end
})

print(#my_cool_container) --> 2
print(rawlen(my_cool_container)) --> 0

never and unknown types

See the RFC never and unknown types for more details.

We’ve added two new types, never and unknown. These two types are the opposites of each other by the fact that there’s no value that inhabits the type never, and the dual of that is every value inhabits the type unknown.

Type inference may infer a variable to have the type never if and only if the set of possible types becomes empty, for example through type refinements.

function f(x: string | number)
    if typeof(x) == "string" and typeof(x) == "number" then
        -- x: never
    end
end

This is useful because we still needed to ascribe a type to x here, but the type we used previously had unsound semantics. For example, it was possible to be able to expand the domain of a variable once the user had proved it impossible. With never, narrowing a type from never yields never.

Conversely, unknown can be used to enforce a stronger contract than any. That is, unknown and any are similar in terms of allowing every type to inhabit them, and other than unknown or any, any allows itself to inhabit into a different type, whereas unknown does not.

function any(): any return 5 end
function unknown(): unknown return 5 end

-- no type error, but assigns a number to x which expects string
local x: string = any()

-- has type error, unknown cannot be converted into string
local y: string = unknown()

To be able to do this soundly, you must apply type refinements on a variable of type unknown.

local u = unknown()

if typeof(u) == "string" then
    local y: string = u -- no type error
end

A use case of unknown is to enforce type safety at implementation sites for data that do not originate in code, but from over the wire.

MyRemoteEvent.OnServerEvent:Connect(function(player: Player, damage: unknown)	
    if typeof(damage) == "number" then	
        -- x: number	
    end	
end)	

Argument names in type packs when instantiating a type

We had a bug in the parser which erroneously allowed argument names in type packs that didn’t fold into a function type. That is, the below syntax did not generate a parse error when it should have.

This breaking change is not enabled yet. We’re going to reach out to the developers using this and enable this change only for Studio. Once we know it’s seldom used, we’ll enable the flag everywhere.

Foo<(a: number, b: string)>

New IntegerParsing lint

For more information, please read this announcement.

We have introduced a new lint called IntegerParsing. Right now, it lints three classes of errors:

  1. Truncation of binary literals that resolves to a value over 64 bits,
  2. Truncation of hexadecimal literals that resolves to a value over 64 bits, and
  3. Double hexadecimal prefix.

Both 1 and 2 are currently not planned to become a parse error, so action is not strictly required here.

For 3, this will be a breaking change! See the rollout plan for details.

New ComparisonPrecedence lint

We’ve also introduced a new lint called ComparisonPrecedence. It fires in two particular cases:

  1. not X op Y where op is == or ~=, or
  2. X op Y op Z where op is any of the comparison or equality operators.

In languages that uses ! to negate the boolean i.e. !x == y looks fine because !x visually binds more tightly than Lua’s equivalent, not x. Unfortunately, the precedences here are identical, that is !x == y is (!x) == y in the same way that not x == y is (not x) == y. We also apply this on other operators e.g. x <= y == y.

-- not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or wrap one of the expressions in parentheses to silence
if not x == y then end

-- not X ~= Y is equivalent to (not X) ~= Y; consider using X == Y, or wrap one of the expressions in parentheses to silence
if not x ~= y then end

-- not X <= Y is equivalent to (not X) <= Y; wrap one of the expressions in parentheses to silence
if not x <= y then end

-- X <= Y == Z is equivalent to (X <= Y) == Z; wrap one of the expressions in parentheses to silence
if x <= y == 0 then end

As a special exception, this lint pass will not warn for cases like x == not y or not x == not y, which both looks intentional as it is written and interpreted.

Function calls returning singleton types incorrectly widened

Fixed a bug where widening was a little too happy to fire in the case of function calls returning singleton types or union thereof. This was an artifact of the logic that knows not to infer singleton types in cases that makes no sense to.

function f(): "abc" | "def"
    return if math.random() > 0.5 then "abc" else "def"
end

-- previously reported that 'string' could not be converted into '"abc" | "def"'
local x: "abc" | "def" = f()

string can be a subtype of a table with a shape similar to string

The function my_cool_lower is a function <a...>(t: t1) -> a... where t1 = {+ lower: (t1) -> a... +}.

function my_cool_lower(t)
    return t:lower()
end

Even though t1 is a table type, we know string is a subtype of t1 because string also has lower which is a subtype of t1's lower, so this call site now type checks.

local s: string = my_cool_lower("HI")

Other analysis improvements

  • string.gmatch/string.match/string.find may now return more precise type depending on the patterns used
  • Fix a bug where type arena ownership invariant could be violated, causing stability issues
  • Fix a bug where internal type error could be presented to the user
  • Fix a false positive with optionals & nested tables
  • Fix a false positive in non-strict mode when using generalized iteration
  • Improve autocomplete behavior in certain cases for : calls
  • Fix minor inconsistencies in synthesized names for types with metatables
  • Fix autocomplete not suggesting globals defined after the cursor
  • Fix DeprecatedGlobal warning text in cases when the global is deprecated without a suggested alternative
  • Fix an off-by-one error in type error text for incorrect use of string.format

Other runtime improvements

  • Comparisons with constants are now significantly faster when using clang as a compiler (10-50% gains on internal benchmarks)
  • When calling non-existent methods on tables or strings, foo:bar now produces a more precise error message
  • Improve performance for iteration of tables
  • Fix a bug with negative zero in vector components when using vectors as table keys
  • Compiler can now constant fold builtins under -O2, for example string.byte("A") is compiled to a constant
  • Compiler can model the cost of builtins for the purpose of inlining/unrolling
  • Local reassignment i.e. local x = y :: T is free iff neither x nor y is mutated/captured
  • Improve debug.traceback performance by 1.15-1.75x depending on the platform
  • Fix a corner case with table assignment semantics when key didn’t exist in the table and __newindex was defined: we now use Lua 5.2 semantics and call __newindex, which results in less wasted space, support for NaN keys in __newindex path and correct support for frozen tables
  • Reduce parser C stack consumption which fixes some stack overflow crashes on deeply nested sources
  • Improve performance of bit32.extract/replace when width is implied (~3% faster chess)
  • Improve performance of bit32.extract when field/width are constants (~10% faster base64)
  • string.format now supports a new format specifier, %*, that accepts any value type and formats it using tostring rules

Thanks

Thanks for all the contributions!

90 Likes

This topic was automatically opened after 10 minutes.

I tend to agree.
(Is this update just for higher convenience when scripting as well as some performance fixes?)

1 Like

Luau gets monthly updates and QoL improvements to make scripting more advanced and convenient

2 Likes

Ah, alright. Thanks for the information.

2 Likes

Whoa! Abosolute banger!
(please make a short explaination for users)

Do you think you can do September and October Recap? September is a click away.
Are there beginner tutorials on LUAU? Just asking.

2 Likes

What an amazing update. The addition of never and unknown is huge, I use this more than I’d like to admit in typescript. Definitely updating my networking library to use this.

The __len methamethod is cool as well, I already have use cases and will implement this in future projects.

Very impressive, thanks to all the contributors & the luau team!

5 Likes

I’ve been searching for a way to make constants, though luau does not seem to currently support it. Is there a way one could take full advantage of this updates regarding constants?

My scripts have never changing variables and if they were sped up in operations, that would be a win. I’m sure there are many other use cases as listed in prior discussions on Devfourms (Lua 5.4).

1 Like

Wow, I just checked and Luau’s %s specifically expected a string instead of calling tostring on everything beforehand as later versions of Lua do. That’s a fun one. Thanks!

2 Likes

Luau does optimize “never changing” variables. From Luau - Compatibility:

2 Likes

Thanks to you I saw their views on <const> on the link.

"<const> in Luau doesn’t matter for performance"

Though I still see the luau creators and/or maintainers thought about room for const.

“If we do end up introducing const variables, it would be through a const var = value syntax, which is backwards compatible through a context-sensitive keyword similar to type.”

Though I cannot easily visualize what this means.

“… there’s ambiguity wrt whether const should simply behave like a read-only variable, ala JavaScript, or if it should represent a stronger contract, for example by limiting the expressions on the right hand side to ones compiler can evaluate ahead of time …”

Usually this is merely read only. Perhaps something like C++'s const?

3 Likes

Really convenient! Reminds me of TypeScript.

1 Like

Very nice recap here!

I do have a question for a syntax feature that I see is needed in Luau. I have been thinking about this for quite awhile now, and I only hope it gets added as I feel like it will make a big difference.

Will there ever be syntax for function parameter defaults, like many other languages?

Here is an example that I was thinking of, and then I’ll show my approach to how I currently have to do this:

function CoolAddFunction(a=1,b=2)
	return a+b
end
function CoolAddFunction(a,b)
	a=if a==nil then 1 else a
	b=if b==nil then 2 else b
	return a+b
end

Both functions should produce the expected results when called using the code below:

print(CoolAddFunction(1,2)) --3
print(CoolAddFunction(1)) --3
print(CoolAddFunction()) --3
print(CoolAddFunction(nil,2)) --3

Keep up the good work! :slightly_smiling_face:

3 Likes

There’s a very clean conventional way to do this in Lua already:

function CoolAddFunction(a,b)
	a = a or 1
	b = b or 2
	return a + b
end

Or if you want to live a bit more dangerously (you might add new usages of the param in the function and forget to deal with the default value):

function CoolAddFunction(a, b)
    return (a or 1) + (b or 2)
end
3 Likes

Discord_waCCa7oJ8o

6 Likes

Cool, thank you for the info. :slightly_smiling_face:

Is the plan to change the type signature of OnServerEvent to use this new type?

image

Obviously in this example I could explicitly typecast to unknown, but that seems like an easy thing to forget about.


It would also be useful for attributes, as they need to be sanitized before use & it’s very convenient to just use them in-line.

--!strict
type Gun = {
	_Ammo: number;

	--Trivialized example: maybe need properties like shooting speed, spread, etc. and then of
	--course some methods...
};

function MakeGunFromModel(model: Model): Gun
	local self = {};
	--This is convenient to write, but it's dangerous; Ammo could be any type, or even nil.
	--Type system, slap me over the head pls.
	self._Ammo = model:GetAttribute("Ammo");
	return self;
end

Wait what? I am confused. :thinking:

I considered doing this, but ended up putting it on the backlog because it needs to be done at a later time. The problem is that implementing this would also affect nonstrict mode when we think it shouldn’t, and we’re actively working to make some serious changes to nonstrict mode. Once that’s done, I can have a look at implementing this change.

2 Likes