Luwu for Luau Users

Luwu's editor tooling supports most Luau and Roblox projects, and brings with it significant diagnostic and developer improvements thanks to Luwu's modified Analysis. This means that luwu-lsp is a drop-in replacement for luau-lsp for most existing Luau code. See Editor Support to set it up.

But if you're ready to make the leap from Luau to Luwu, all the runtime stuff on this page is for you. Almost all pure Luau 0.730 runs in Luwu as well, but Luwu adds some features to make code more enjoyable to write and debug.

Depending on how you use Luwu, it could look like a Luau++ or a completely different language altogether.

What Luwu is for

Luwu is built for code that models behavior and data, such as:

Features

Your first program

Luwu files end in .luwu. If you've written Luau, this short program should look familiar. It holds a small inventory, picks out the fruit that's in stock and prints it cheapest first:

type Item = { name: string, price: number, stock: number }

const inventory: { Item } = {
    { name = "apple", price = 1.25, stock = 12 },
    { name = "pear", price = 0.8, stock = 0 },
    { name = "kiwi", price = 0.5, stock = 30 },
}

const in_stock = { for _, item in inventory when item.stock > 0 give item }
table.sort(in_stock, function(a: Item, b: Item)
    return a.price < b.price
end)

for _, .{ name, price } in in_stock do
    print(`{name}: {price}`)
end

Most of it is the Luau you already know. The { for ... when ... give item } line is a table comprehension: it builds the filtered array in a single expression. In the second loop, for _, .{ name, price } uses destructuring to pull the fields you need out of each item. You don't need two more lines of code to put item.name and item.price into local variables.

Run fruit.luwu with seal, a Luau and Luwu runtime:

seal ./fruit.luwu
kiwi: 0.5
apple: 1.25

Luwu and Luau

Luwu was forked by the hina, nyla, & ferris (HNF) Discord community to better suit our needs for general-purpose programming outside of Roblox.

In addition to popular Luau features such as string interpolation, generalized iteration, compound assignment operators, the new type solver (with type functions!), const syntax, if local, and more, Luwu code also features highly optimized classes and objects with code reuse facilitated via traits, table/object/userdata destructuring, do expressions, table comprehensions, the none primitive, externally managed buffers, and more.

Since the fork, Luau and Luwu have diverged. The main differences today are in classes and inheritance, integers, and destructuring.

See Compatibility →

The sections below give a short tour of each feature.

Types and editor support

We maintain our LSP and the Luwu VSCode/Code-OSS extension alongside the language. New editor features and type checking improvements are developed directly against them so we see how every change reads in a real editor before it ships.

That's also why Luwu prints types and errors differently from Luau. Luau is primarily developed against Roblox's Studio script editor and stringifies types to fit the single-line diagnostics Studio shows. Our language server targets VSCode/Code-OSS (with hopefully more editors coming soon!) where hovers and diagnostics span several lines. Luwu prints types over multiple lines formatted like code and writes its error messages for people reading them in a hover. We've also added context to Luwu's type errors that makes them easier for tools to work with.

Type Checking

Luwu enables type checking by default to catch your code's mistakes before you run it. Luwu extends Luau's new type solver to add new features, drastically improve error message friendliness and readability, and catch more common mistakes.

Luwu type checking defaults to a relatively strict mode, but you can go back to the autocomplete-only Luau default with --!nonstrict (or all the way to --!nocheck for untyped Lua code). Note that due to popular demand, we surface typos even in --!nonstrict.

See Type Checking →

Literal Types and Validators

A literal type requires a value written directly at the call, such as "#3b5bdb" or "1.5rem", and a validator is a type function that checks that literal during analysis. Your own APIs can use them to reject a malformed color, size or route in the editor with an error message you write yourself.

See Literal Types and Validators →

Lints

We also expand Luau's built-in lint system with more than 40 new lints for Luau and Luwu code to help you write more idiomatic and performant code.

See Lints →

Better Hovers

Our hovers target luwu-lsp and the Luwu VSCode/Code-OSS extension and significantly improve recursive type stringification. A hover exposes every relevant type with verbosity levels and go to location links you can click. Overloads and unions are serialized in a more readable way, and each type with documentation attached shows one line of it above.

See Better Hovers →

Objects and data

Classes and Traits

A class represents a domain specific concept for your program, such as Player and Health classes in a game. In Luwu, you can use classes to group related data and behavior into components and use traits to share functionality that multiple classes need to use.

Classes are also great for representing data that relies on certain properties always being valid, such as health being clamped between 0 and max_health, age not being negative, positions not being NaN, etc. This Health example represents the concept of health points, protects the invariant that health must always be valid and has a healthbar display for when you print it out to a terminal:

class Health private (
    private current: number,
    public const max_health: number
)
    public function full(max_health: number): Health
        return Health(max_health, max_health)
    end
    private function clamped(current: number, max_health: number): Health
        if math.isnan(current) then
            error("health can't be NaN", 3)
        end
        return Health(math.clamp(current, 0, max_health), max_health)
    end
    public function __add(self, amount: number): Health
        return Health.clamped(self.current + amount, self.max_health)
    end
    public function __sub(self, amount: number): Health
        return Health.clamped(self.current - amount, self.max_health)
    end
    -- ... comparisons (__eq, __lt, __le) and a healthbar __tostring
end

local hp = Health.full(100)
print(hp)
hp -= 65
print(hp)
hp += 500
print(hp, hp == Health.full(100))
hp -= 90
print(hp, hp < Health.full(20))

The healthbar's colors come from seal's @std/io/colors library. Save the full example as health.luwu and run it with seal:

seal ./health.luwu
████████████████████ 100/100
███████░░░░░░░░░░░░░ 35/100
████████████████████ 100/100	true
██░░░░░░░░░░░░░░░░░░ 10/100	true

See the full example on the Classes and Traits page →

Destructuring

Destructuring pulls several fields into locals at once from anything you can index with a dot: tables, objects, classes and userdata. It's similar to destructuring assignment in JavaScript and destructuring patterns in Rust. Use it in local and const declarations:

const player = { name = "Ada", level = 12 }
const .{ name, level } = player
print(name, level)

And in a for loop's header:

const tasks = {
    { title = "Feed Nyla", done = true },
    { title = "Write docs", done = false },
}
for _, .{ title, done } in tasks do
    print(title, done)
end

See Destructuring →

Restructuring

The same syntax on the right of = builds a table instead: .{ name, age } is equivalent to { name = name, age = age }. It's similar to shorthand properties in JavaScript and field init shorthand in Rust. Class.{ name, age } builds a class without a primary constructor the same way.

const name, age = "Ada", 36
const person = .{ name, age }
print(person.name, person.age)

The none Primitive

none means "intentionally empty". Unlike nil, you can store it in a table.

const list = { "first", none, "third" }
print(#list)

See The none Primitive →

Everyday code

Control Flow

Blocks open with a keyword like then or do and close with end. false, nil and none are the only values that count as false in a condition:

for i = 1, 3 do
    if i == 2 then
        continue
    end
    print(i)
end

See Control Flow →

Tables

A table stores values under keys. The same table type holds arrays, maps and records, and array indices start at 1:

const fruits = { "apple", "kiwi", "pear" }
const gold: { [string]: number? } = { Ada = 120, Grace = 75 }
print(`{fruits[1]} costs {gold.Ada}`)

See Tables →

Functions

Functions are values. They can return several values, be passed to other functions and keep the locals around them alive as closures:

const function min_max(a: number, b: number): (number, number)
    return math.min(a, b), math.max(a, b)
end

const low, high = min_max(9, 4)
print(`{low} to {high}`)

See Functions →

Expressive Control Flow

Expressive control flow is a set of features that let you get a value, check it and decide what to do with it in one expression. Combined, they make Luwu a more expression-focused language than Python or Luau and make data transformation code much easier to read.

if local and if const bind a value inside the condition and when adds more checks that can use it:

const users: { [string]: { name: string, admin: boolean }? } = {
    ada = { name = "Ada", admin = true },
    grace = { name = "Grace", admin = false },
}
if const user = users["ada"] when user.admin then
    print(`{user.name} is an admin`)
end

Loops take when chains too and skip the items that fail them:

const prices: { [string]: number? } = { apple = 1.25, pear = 0.8 }
for
    _, fruit in { "apple", "kiwi", "pear" }
    when const price = prices[fruit]
do
    print(`{fruit} costs {price}`)
end

A do expression runs statements anywhere an expression goes and gives back a value. In Luwu we prefer do/give blocks over temporary and intermediate variables. They're useful in a lot of places:

const scores = { 72, 95, 88 }
const best = do
    local top = 0
    for _, score in scores do
        top = math.max(top, score)
    end
    give top
print(best)

or return, or continue and or break leave as soon as a value is missing:

const function describe(fruit: string): string
    const price = prices[fruit] or return `{fruit} isn't sold here`
    return `{fruit} costs {price}`
end

See Expressive Control Flow →

Table Comprehensions

A table comprehension builds a new table from a loop in one expression.

const numbers = { 1, 2, 3, 4, 5, 6 }

const even_squares = { for _, n in numbers when n % 2 == 0 give n * n }
print(#even_squares)

See Table Comprehensions →

Default Arguments

Function parameters and class constructor parameters can have default values:

const function greet(name: string, greeting = "Hello")
    print(`{greeting}, {name}!`)
end

greet("Ada")

See Default Arguments →

Named functions in table literals

In Luau, a function stored in a table is a key assigned an anonymous function:

local handlers = {
    on_start = function()
        print("started")
    end,
}
handlers.on_start()

In Luwu, you can declare it like any other function by writing function name() inside the table:

const handlers = {
    function on_start()
        print("started")
    end,
    function on_stop()
        print("stopped")
    end,
}

handlers.on_start()
handlers.on_stop()

It reads the same as a const function or a class method, and a function without a name is still an array item as in Luau.

Importing classes with their types

In Luau, a value and a type are imported separately. A class-like table needs both lines:

local shapes = require("./shapes")
local Circle = shapes.Circle
type Circle = shapes.Circle

A Luwu class is a value and a type at once. Given a module that exports one:

export class Circle(radius: number)
    function area(self): number
        return math.pi * self.radius ^ 2
    end
end

Importing the class brings its type along, whichever way you import it:

const shapes = require("./shapes")
const Circle = shapes.Circle
const .{ Circle } = require("./shapes")

const function describe(circle: Circle): string
    return `a circle with area {circle:area()}`
end

Traits work the same way. type Circle = shapes.Circle still works, but isn't needed.

Libraries and embedding

Library Extensions

Luwu adds a few library functions. table.drop removes items from an array by value. Unlike table.remove(t, table.find(t, x)), it doesn't remove the last item when x is missing.

const party = { "Ada", "Grace", "Alan" }

table.drop(party, "Grace")
print(#party)

More are planned, like table.keys, table.extend and string.startswith.

See Library Extensions →

Userdata (Extern Types)

Userdata gives scripts objects that the host program defines in C, C++ or Rust and owns. The host declares an extern type for each one, and the analyzer checks scripts against it.

See Userdata (Extern Types) →

External Buffers and Strings

Embedders can expose huge files to scripts without cloning them into the VM.

See External Buffers and Strings →

Generic Extern Types

Because extern types and their methods take generic parameters, a host can declare one Result<T> for every call that returns one.

See Generic Extern Types →

Experimental Features

Push and pop operators and match expressions work today, but their design may still change.

See Experimental Features →