Expressive Control Flow

Luwu adds a few control flow tools that Luau doesn't have to improve expressiveness and reduce visual overhead.

if local

In Luau, you often get a value, then check that it exists:

const scores: { [string]: number? } = { ada = 92 }

local score = scores["ada"]
if score then
    print(`Ada scored {score}`)
end

With if local, you bind the value inside the condition:

const scores: { [string]: number? } = { ada = 92 }

if local score = scores["ada"] then
    print(`Ada scored {score}`)
end

The branch runs when the value is truthy: not nil, false or none.

score only exists inside the branch and can't leak into the code after it.

Use if const when the branch won't reassign the value. Most of the time, that's what you want.

type User = { name: string }
const users: { User } = { { name = "Ada" }, { name = "Grace" } }

const function find(name: string): User?
    for _, user in users do
        if user.name == name then
            return user
        end
    end
    return nil
end

if const user = find("Grace") then
    print(`Found {user.name}`)
elseif const fallback = find("Ada") then
    print(`Using {fallback.name} instead`)
else
    print("Nobody here")
end

when chains

Add more checks with when. Each clause must pass for the branch to run.

A clause can be a plain condition or a binding that later clauses can use.

const players: { [string]: { level: number, guild: string? } } = {
    ada = { level = 12, guild = "Builders" },
    grace = { level = 3 },
}

const function invite(id: string)
    if
        const player = players[id]
        when player.level >= 10
        when const guild = player.guild
    then
        print(`{id} can lead raids for {guild}`)
    else
        print(`{id} can't lead raids yet`)
    end
end

invite("ada")
invite("grace")

The chain checks its clauses left to right and stops at the first one that fails.

A binding clause can destructure. Every name it binds must be truthy.

const function parse_point(text: string): { x: number?, y: number? }
    const x, y = string.match(text, "^(%-?%d+),(%-?%d+)$")
    return { x = tonumber(x), y = tonumber(y) }
end

if const .{ x, y } = parse_point("3,4") then
    print(x + y)
end

if const in expressions

if local and if const also work in if-else expressions.

const nicknames: { [string]: string? } = { grace = "Amazing Grace" }

const function display_name(id: string): string
    return if const nickname = nicknames[id] then nickname else id
end

print(display_name("grace"), display_name("ada"))

when in loops

Luwu's loops take when chains too.

while local

while local binds a fresh value on every pass. The loop ends as soon as the chain fails.

const jobs = { "build", "test", "deploy" }

while const job = table.remove(jobs, 1) do
    print(`Running {job}`)
end

Add a when clause to stop early:

const lines = { "name = Ada", "level = 12", "", "ignored = true" }
local index = 0

while const line = lines[index + 1] when line ~= "" do
    index += 1
    print(line)
end

for ... when

Instead of wrapping the whole loop body in an if, put a when chain after a for loop's header to skip the items that fail it.

const inventory = {
    { name = "Arrow", count = 24 },
    { name = "Potion", count = 0 },
    { name = "Rope", count = 2 },
}

for _, item in inventory when item.count > 0 do
    print(`{item.name} x{item.count}`)
end

Bindings work here too, and so do numeric loops:

const prices: { [string]: number? } = { apple = 1.25, pear = 0.8 }
const basket = { "apple", "kiwi", "pear" }

for _, fruit in basket when const price = prices[fruit] do
    print(`{fruit} costs {price}`)
end

for n = 1, 20 when n % 3 == 0 and n % 2 == 1 do
    print(n)
end

do expressions

Sometimes one value takes several statements to work out. A do expression runs a block and gives back a value.

give ends the block and produces its value. There's no end.

In Luwu we prefer do/give blocks over temporary and intermediate variables. Scoping the computation inside the expression that produces a value makes it obvious what the code is building up. A do expression also fits several statements where only an expression goes, like do print(x) give x to debug a value in place. Unlike a function call that abstracts the same computation, it's always inlined and costs nothing.

In Luau, the temporaries sit next to the result and stay in scope for the rest of the function:

const price, quantity = 4.5, 12

local subtotal = price * quantity
local discount = if quantity >= 10 then subtotal * 0.1 else 0
local total = subtotal - discount
print(total)

With a do expression, subtotal and discount live inside the block that computes total. Readers see at a glance that they exist only for it:

const price, quantity = 4.5, 12

const total = do
    const subtotal = price * quantity
    const discount = if quantity >= 10 then subtotal * 0.1 else 0
    give subtotal - discount
print(total)

A loop that builds up one value works the same way:

const words = { "luwu", "classes", "traits" }

const longest = do
    local best = ""
    for _, word in words do
        if #word > #best then
            best = word
        end
    end
    give best

print(longest)

Because the temporary best stays inside the block, longest can be const.

A give can also leave the block early, like a return. This one stops at the first player who is online and only falls back to the last line when nobody is:

const players = {
    { name = "Ada", online = false },
    { name = "Grace", online = true },
    { name = "Alan", online = true },
}

const host = do
    for _, player in players do
        if player.online then
            give player.name
        end
    end
    give "nobody"
print(host)

or return, or continue, or break

With or return, a guard clause that checks for a missing value and leaves early fits on one line.

const prices: { [string]: number? } = { apple = 1.25, pear = 0.8 }

const function total(order: { [string]: number? }): number?
    local sum = 0
    for item, count in order do
        const price = prices[item] or return nil
        sum += price * count
    end
    return sum
end

print(total({ apple = 4 }))
print(total({ apple = 1, kiwi = 2 }))

After the guard, Luwu knows price isn't nil.

or continue and or break work the same way inside loops:

const entries = { "hp=10", "broken", "mp=4", "stop", "xp=99" }

for _, entry in entries do
    if entry == "stop" then
        break
    end
    const key, value = string.match(entry, "^(%a+)=(%d+)$")
    if not key or not value then
        continue
    end
    print(key, value)
end

for _, entry in entries do
    entry ~= "stop" or break
    const key, value = string.match(entry, "^(%a+)=(%d+)$")
    key and value or continue
    print(key, value)
end

Both loops print the same thing. The second one reads top to bottom with every exit at the left edge.