Luau Type Checking Beta

Hello!

We’ve been quietly working on building a type checker for Lua for quite some time now. It is now far enough along that we’d really like to hear what you think about it.

I am very happy to offer a beta test into the second half of the Luau effort.

[Originally posted on the Roblox Developer Forum.]

Beta Test

First, a word of caution: In this test, we are changing the syntax of Lua. We are pretty sure that we’ve mostly gotten things right, but part of the reason we’re calling this a beta is that, if we learn that we’ve made a mistake, we’re going to go back and fix it even if it breaks compatibility.

Please try it out and tell us what you think, but be aware that this is not necessarily our final form. 🙂

Beta testers can try it out by enabling the “Enable New Lua Script Analysis” beta feature in Roblox Studio.

Overview

Luau is an ahead-of-time typechecking system that sits atop ordinary Lua code. It does not (yet) feed into the runtime system; it behaves like a super powerful lint tool to help you find bugs in your code quickly.

It is also what we call a gradual type system. This means that you can choose to add type annotations in some parts of your code but not others.

Two Modes

Luau runs in one of two modes: strict, and nonstrict.

Nonstrict Mode

Nonstrict mode is intended to be as helpful as possible for programs that are written without type annotations. We want to report whatever we can without reporting an error in reasonable Lua code.

  • If a local variable does not have a type annotation and it is not initially assigned a table, its type is any
  • Unannotated function parameters have type any
  • We do not check the number of values returned by a function
  • Passing too few or too many arguments to a function is ok

Strict Mode

Strict mode is expected to be more useful for more complex programs, but as a side effect, programs may need a bit of adjustment to pass without any errors.

  • The types of local variables, function parameters, and return types are deduced from how they are used
  • Errors are produced if a function returns an inconsistent number of parameters, or if it is passed the wrong number of arguments

Strict mode is not enabled by default. To turn it on, you need to add a special comment to the top of your source file.

--!strict

New syntax

You can write type annotations in 5 places:

  • After a local variable
  • After a function parameter
  • After a function declaration (to declare the function’s return type)
  • In a type alias, and
  • After an expression using the new as keyword.
local foo: number = 55

function is_empty(param: string) => boolean
    return 0 == param:len()
end

type Point = {x: number, y: number}

local baz = quux as number

Type syntax

Primitive types

nil, number, string, and boolean

any

The special type any signifies that Luau shouldn’t try to track the type at all. You can do anything with an any.

Tables

Table types are surrounded by curly braces. Within the braces, you write a list of name: type pairs:

type Point = {x: number, y: number}

Table types can also have indexers. This is how you describe a table that is used like a hash table or an array.

type StringArray = {[number]: string}

type StringNumberMap = {[string]: number}

Functions

Function types use a => to separate the argument types from the return types.

type Callback = (string) => number

If a function returns more than one value, put parens around them all.

type MyFunction = (string) => (boolean, number)

Unions

You can use a | symbol to indicate an “or” combination between two types. Use this when a value can have different types as the program runs.

function ordinals(limit)
    local i = 0
    return function() => number | nil
        if i < limit then
            local t = i
            i = i + 1
            return t
        else
            return nil
        end
    end
end

Options

It’s pretty commonplace to have optional data, so there is extra syntax for describing a union between a type and nil. Just put a ? on the end. Function arguments that can be nil are understood to be optional.

function foo(x: number, y: string?) end

foo(5, 'five') -- ok
foo(5) -- ok
foo(5, 4) -- not ok

Type Inference

If you don’t write a type annotation, Luau will try to figure out what it is.

--!strict
local Counter = {count=0}

function Counter:incr()
    self.count = 1
    return self.count
end 

print(Counter:incr()) -- ok
print(Counter.incr()) -- Error!
print(Counter.amount) -- Error!

Future Plans

This is just the first step!

We’re excited about a whole bunch of stuff:

  • Nonstrict mode is way more permissive than we’d like
  • Generics!
  • Editor integration

Updated: