Luau Recap: August 2020

That’s not what type assertions are. That’s just normal type annotations, which would and should produce invalid code by the way. Type assertions would use the as keyword where the user knows they may be doing something unsafe. So your example would be valid with type assertions but unsafe, but that’s on the user for using type assertions:


local storage = {}
function foo(): Array<BasePart>
    return storage
end
local x: Array<Instance> = foo() as Array<Instance>
table.insert(x, Instance.new("IntValue"))
foo()
3 Likes

Either way works.

This is matching Lua grammar so probably not something we can change easily. It’s not great though as it obscures failure to escape backslashes. Will see if we can issue a warning here.

Lua 5.3.4  Copyright (C) 1994-2017 Lua.org, PUC-Rio
> print("\LOL")
stdin:1: invalid escape sequence near '"\L'

At least as of Lua 5.3 it is a syntax error to use an invalid escape.

Eh, maybe. Either way I think it’s correct to flag this - the linting pass is allowed to be more strict than the runtime behavior to prevent accidental mistakes, e.g. we treat % escapes of non-magical symbols as invalid even though the manual explicitly says it’s fine, because it’s possible that this is unintentional. In this case it feels like %1 is very likely to be accidental and should be either replaced with %0 or a missing capture should be added, e.g. maybe you forgot it!

The release next week will fix all of the other issues you brought up, thanks!

It is as of 5.2.

But I do want to note that Luau policy on backwards compatibility is drastically different from Lua 5.x policy on backwards compatibility which seems to be “if new Lua release doesn’t break at least one widely known Lua program, we aren’t ambitious enough” :stuck_out_tongue_winking_eye: :

It would make sense to gather analytics on this. If this is currently not violated by any existing scripts in Roblox we can change the behavior.

1 Like

My bad, I didn’t realize that this had changed since 5.1. Makes sense that it probably can’t change then.

Is there a way to assert a declared type that I’m not aware of? Or is that what the as keyword was supposed to do?

--!strict

type Foo = {Foo: string}
type Bar = {Bar: string}

local function Func(v: string|BrickColor|Foo|Bar): string
	if type(v) == "string" then -- Can assert primitive type.
		return v
	elseif typeof(v) == "BrickColor" then -- Can assert nominal type.
		return v.Name
	elseif ... then -- Want Foo, how to assert?
		return v.Foo
	elseif ... then -- Want Bar, how to assert?
		return v.Bar
	end
end

print(Func("string"))
print(Func(BrickColor.Red()))
print(Func({Foo="foo"}))
print(Func({Bar="bar"}))

There isn’t, as types don’t have a runtime representation (right now typeof will actually work here as far as typechecker is concerned but it’s a bug that’s on our list to fix)

1 Like

It would really be nice to have a language server that I can use with my vim’s intellisense plugin. I have noticed options like this, but it’s very heavy and not ideal at all. I’m currently stuck using lua-lsp. It’s nice, but it really isn’t ideal for Roblox development. A customizable formatter to go along with that would be really nice as well.

Is Roblox-CLI something that might come soon? As an Arch Linux user, I’ve resigned myself to the fact that I won’t really be able to feasibly use Roblox (and Roblox studio is slow and laggy as heck on grapejuice). I think something Roblox misses when it comes to even partial support for Linux distributions is the fact that, even though the playerbase on Linux distros is low, the developer count is significantly higher. And I don’t think I need to remind people that developers are what make Roblox run.

In the end, the things I’m hoping for are…

  • Luau language server and formatter
  • Roblox-CLI
  • Some way to transfer files to game without ever opening Studio (rojo comes close, but studio needs to be open)
  • Straight up Roblox support (not happening)

Is there any place I can see the plans for development in these areas (if there are any at all)?

9 Likes

It seems that the type optionality operator is non-terminal, is this intentional?

local a: number???? = 6 --> Works
4 Likes

Type statements are not parsed correctly when you omit whitespace thanks to misinterpretation of the >= token:

type Foo<T>=number --> Expected `>` ...
6 Likes

Wondering about the following type definition:

type Foo = {[nil]: string}

Table keys are not allowed to be nil, so this definition can only be satisfied by an empty table, which may or may not be useful. It also allows only nil to be indexed:

local foo: Foo = {}
print(foo[nil]) --> nil

Additionally, an optional type can be used as a key:

type Foo = {[string?]: string}

Obviously, you still can’t assign to the nil key, but you can still get from it, which may or may not be useful:

local foo: Foo = {bar="baz"}
print(foo["bar"]) --> baz
print(foo[nil]) --> nil

My question is, should nil type keys be disallowed, or are these behaviors useful enough to keep them as-is? I could see the second definition being useful where the user wants to index the table with an optional type. But the first definition, where the key is just nil, seems rather quirky.

2 Likes

The type checker seems to ignore the values of a constructed table when the table value type is defined as nil:

type Foo = {[string]: nil}
local foo: Foo = {["foo"] = 42} -- No warning
foo.bar = 42 --> Type mismatch nil and number

Additionally, The following causes a crash, but only when !strict is enabled:

--!strict
type Foo = {[string]: nil, String: nil}
local foo: Foo = {["foo"] = 42}
2 Likes

An implicit assertion is missed. Consider the following code:

type Foobar = {Foobar: string}

local function Func(v: Foobar|string): string
	if typeof(v) == "string" then
		return v
	end
	return v.Foobar --> Type Foobar | string does not have key 'Foobar'
end

print(Func("string"))
print(Func({Foobar="foobar"}))

The conditional correctly asserts v as string, as expected. Beyond the conditional branch, the only possible type v could be is Foobar. However, this is not detected.

2 Likes

Yeah we didn’t think that this was important enough to be invalid syntax, but we should make a lint rule for this and string | number? which reads weirdly. You’d silence this warning by writing (string | number)? instead.


Lua 5.4 does the same thing! local n<const>=1 is invalid syntax there.

We should nonetheless make this work.

2 Likes

I have found another issue with format string analysis:
image
[%%] is a valid pattern, but presumably it thinks the second % escapes the ]?

1 Like

I’ve been using typed Luau a bunch in the last few days to rewrite an older project (you’ve probably seen me post a few bug reports and feature requests in the last several days) and one thing I will say stands out is how hard it is to work with without an equivalent to as.

The bulk of the work has thus far been on modernizing an API tool so that I can easily access information about classes and their members, and part of that involves writing type aliases for the API dump JSON. That in of itself was easy enough, but the API dump has a few optional fields in places.

The finished type alias for something like a class member ended up looking something like this:

type Member = {
  Name: string,
  MemberType: string,
  Security: string | { Read: string, Write: string },
  Tags: Array<string>?
}

Note: this isn’t the entire alias as it’s quite large and not important to this post

The ones to focus on are Security and Tags.

In order to work with Tags, I had to do a truthy check on it, which while fine, has an annoying bug that I hope is fixed soon (:eyes:).

Security is a different beast though. At a glance, it seems easy to handle:

if type(Security) == "string" then
  -- Security will be a string here!
else
  -- Security will be a table!
end

This turned out to not work though, which turned out to be a major bummer. I’m not sure if this is a bug or not – if it is, I’ll file a bug report as soon as I’m told.

Refining the type from there is… Outright impossible. To clarify, in a strict environment, to my knowledge it is currently impossible to refine a type such that it will only be a table. Something like typeof(Security) == "table" doesn’t work, and you can’t directly index Security to check if it’s a table because then the script analyzer will raise an error about Read not being a valid member of string.

I ended up solving this problem by just passing the table through to a non-strict module which solved the problem with a simple if-statement, but that’s not ideal! We need some way to tell the type system that something is a particular type, because it’s currently not smart enough to know these things (and to be honest it’s a bit much to expect it to ever be perfect).

I once thought that a simple type assertion operator (! in TypeScript) would be enough but I’m no longer of that opinion. I realize there’s no easy way to have as be a valid keyword because you can call functions like as{} but there has to be a solution here.

While talking with a few people a while back, we came up with a few potential solutions, ranging from something as simple as requiring expressions be wrapped in parentheses (e.g. (foo as {}) vs foo as {}) or introducing a symbol (@ might be good but there be dragons). Just… please add some way to do it.

3 Likes

Thanks - yeah the new scanner for sets didn’t carefully handle %] when the first % was escaped. Will be fixed next week.

I don’t think having optional types in table indexers’ key is ever useful enough to warrant some use cases like that. We should warn on it, thanks for bringing this up.


Definitely a bug. For what it’s worth, the expression {["foo"] = 42} actually produces the type {foo: number} in strict mode, {foo: any} in nonstrict. Not a table with an indexer whose key is string, producing a number.

This is fixed in 445!


Yeah, this is because Luau doesn’t do any real flow analysis right now. It’s on the roadmap too.

3 Likes

Lovin’ LuaU!

For those who want, here are some useful data types:

type array<t> = {[number]:t}
type dictionary<t> = {[string]:t}

-- Examples:
local myTableA:array<string> = {"a","b","c"}
local myTableB:dictionary<any> = require(module)