Luau Type Checking Beta!

What makes adding continue as keyword different from adding type as a keyword? type already is a variable, it has a value and can be reassigned and just used as a variable, but now it’s also a keyword. continue is already sometimes used as a variable, but I think any context where it is intended as a keyword is uniquely distinguishable from any context from where it is intended as a variable. e.g.

for i = 0, 1 do
	if i == 0 then
		continue
	end
end

this makes no sense if continue is not treated as a keyword, just like continue = true makes no sense if it is.
this makes sense:

for i = 1, 10 do
	local continue = i%2 == 0
	if continue then
		continue
	end
end
7 Likes

Right, when I said “keywords” above, I meant “true” keywords - not context-sensitive keywords (which type is one of, now). You are correct in that we could explore the option of continue becoming a context-sensitive keyword.

The challenge is that, since Lua doesn’t have statement terminators, and continue statement should work in isolation, after parsing continue in the beginning of the statement, the parser has to make sure that the next token isn’t starting a new statement.

What if the next statement starts from a parenthesis (()?

This is valid Lua:

function continue(f)
   return f
end

function foo()
    return print
end

for i=1,3 do
    continue(foo())(i)
end

If we introduce continue as a context-sensitive keyword, we would have two options:

  1. Parse continue in the loop body as a new loop continuation statement. This can break code since it changes semantics of the (valid) code above
  2. Say that since continue is followed by (, it’s a function call. This keeps compatibility, but makes code like this behave in surprising ways:
function foo()
    return print
end

for i=1,3 do
    continue
    (foo())(i)
end

Which is of course the same code as above, but it doesn’t work anymore because continue variable is nil.

4 Likes

(I should note that perhaps “continue that is followed by ( is a function call” is a reasonable compromise, and we already have a lint for the possible issue with parentheses on the new line, so it may be a viable path - but I wanted to highlight that it’s not as simple as it may sound like)

4 Likes

Experiencing a lot of random crashes with this feature.

1 Like

I think that your example isn’t ambiguous. continue should behave like break and return in that no statement can go after it in the same block. In your example it only makes sense to interpret the continue as a function call. if you wrote continue; (foo())(i) then it should error because there’s a statement after the continue.

also (unrelated) this is completely valid luau code

--!strict
type type={type:any}
local function type(type:type)=>type return type end
print(type{type=type})
10 Likes

Yeah, that’s fair. Meaning that programmers who write code would never expect continue on a separate line to work. Maybe continue is not doomed after all!

21 Likes

I think it does. From Lua to Luau, you have to start keeping track of scoping in order to determine whether type is a variable or a keyword. Consider the following snippets compiled with the Luau parser:

type Foo = nil -- `type` is a keyword.

vs.

local type
type Foo = nil -- Now it's a variable.

--> Expected '=' when parsing assignment, got 'Foo'

This might be fine for a parser that already plans on running the code, but it will be a non-trivial step up for syntax highlighters, beautifiers, linters, and so on. Programs that had enough foresight to separate the parser and scoping will now have to couple them together. On top of that, the documentation will have to somehow explain this concept to novice programmers and Lua veterans alike. It’s not as simple as “This word is a keyword only when the next thing is an identifier.”

The feeling I get overall is that this new syntax is being justified by the implementation. Roblox’s parser already handles scoping and a bunch of other things, so of course it’s trivial to add context-sensitive keywords. But that doesn’t make the concept itself trivial. This is why I keep asking for a specification. Writing a spec approaches the idea from a different angle, and reveals problems that would otherwise go unnoticed.


Bonus snippet:

local Foo, number = 1, 2
local _ = _ and
	type Foo = number
	type Foo = number
5 Likes

Small issue for me. It seems to be impossible to define the following “doubly” dependant types:

type Connection = {NodeA: Node, NodeB: Node} -- Depends on Node below
type Node = {Connections: Array<Connection>} -- Depends on Connection above
type System = {Nodes: Array<Node>}

I can’t just define Connection again or I get an error: cannot multiply define type

How would I do this with this type system? Is it possible?

Edit:
The following is possible, however it feels a bit hacky

type _Connection<Node> = {A: Node, B: Node}
type Node = {Connections: Array<_Connection<Node>>}
type Connection = _Connection<Node>
type System = {Nodes: Array<Node>}
4 Likes

As for where this addition is headed, I love it a TON! It could allow me to make executing user code with my sandbox so much more safe and will allow me to create very strict APIs which can be returned by user code. Very cool indeed!

This would require invalid types causing an error though which doesn’t seem to occur at all in command bar. Additionally, I have no idea how this will behave with loadstring. Could be nasty.

1 Like

We have a BNF internally for this but it won’t help you much because BNFs are not simple to parse.

You don’t have to track scoping (unless I misunderstand what you mean by scope), but you are entirely correct in that to correctly syntax-highlight the source, it’s no longer sufficient to write a lexer - you need a parser.

However, this is a general property of languages with context-sensitive keywords. I don’t see a way out of this due to the backward compatibility issues I mentioned before.

3 Likes

Would it be ever possible to use this outside Roblox???

2 Likes

This is a bug :slight_smile: We’ll fix this.

3 Likes

I found an interesting bug.
image
image
image

This seems to maybe be revealing some kind of internal naming somehow? Someone experienced something similar near the beginning of this post with CFrames.

Edit:
It actually appears that maybe setmetatable sets a __meta property temporarily??? I can’t test this since the only way to test this is metatables and metatables aren’t fired if this is the case. Would explain why setmetatable doesn’t accept userdatas (can’t rawset a value on a userdata).

2 Likes

The editor is giving me warnings for using table.pack and table.unpack:
image
table.pack's telling me: W000: This function must be called with self. Did you mean to use a colon instead of a dot?
table.unpack's telling me: W000: Type mismatch (free22841-3, free22841-3) => boolean | nil and number

5 Likes

Didn’t see this specific message posted here yet after scrolling though, sorry if its already posted and I missed it:

2 Likes

I am not going to lie, I dislike the syntax in its entirety, it could have been done way better and it isn’t what I wanted.

I was hoping for code that looked more like this:

local custom_types = {
type entity_type = {name = string; character = Instance; health = integer;}
}

custom_types.entity_type new_entity(name = “john”, character = Instance.new(“Model”); health = 100);

I wished you did more than making Lua look like Rust.

4 Likes

First of all, I want to say that I am really glad they finally released this, I had been looking forward to this update. Although I think it still has some issues with Object Oriented code.
It is unable to find methods of an object when those methods are set using : instead of the regular . I am not sure if that’s a problem on my part or if it has not been implemented yet.
image

Although I am really hyped to check this all out :slight_smile:

3 Likes

What are the performance implications for this update? In (generally) faster lower level languages, you declare types so the compiler knows how much memory it needs to allocate to store a variable. One of the features of a higher level interpreted language is the ability to have a variable which can be declared as one type and change to another type. Another feature is ease of entry: you don’t even have to understand the difference between two different types.

In the case of this update, it does not seem like it was done for performance improvement as “number” is not something that can be used to determine a variable’s size. This is obviously a feature for convenience.

5 Likes

I am pretty sure they introduced so it’s easier for developers to work together on the same code. It’s for consistency and convenience plus it makes code easier to read.

2 Likes

Yeah, I get that. I was only questioning what impact this would have on runtime performance. Would performance be sacrificed for this convenience?

1 Like