lloyAlloy on GitHub
Operators
Docs

Operators

Updated

The operators Alloy adds to Luau, each with its fixed emit.

->

a->b

Child lookup: a:FindFirstChild("b"), nil when the child is missing and nil when a is, so it takes no ?: write a->b where a?:FindFirstChild("b") stood. The right side is a Name, a string, an interpolated string, or [expr].

After a parameter list, -> T is the return type.

:

c ? a : b

The else of a ternary. Touching a name it is a method call, obj:method(); without the call, obj:method is the method bound to obj, a function to pass along; after a name with a space it starts a type, x: T.

!

a!

Non-nil assert. Throws when a is nil, with the source text in the message. The type loses its ?.

Emits if a == nil then error(...) else a on a temp.

?

local n = 3
local label = n > 2 ? "big" : "small"

The ternary: c ? a : b is if c then a else b. A : that touches its receiver, x:upper(), stays a method call inside the branches; the else marker has a space before it.

?:

a?:m(x)

Safe method call: nil when a is nil, and x does not evaluate. Else a:m(x).

??

a ?? b

Nil coalescing: a when it is not nil, else b. Unlike or, false passes through.

??=

a ??= b

Assigns b to a when a is nil. An index on the left runs once.

Emits if a == nil then a = b end.

?.

a?.b

Safe index: nil when a is nil, else a.b. The ? guards the rest of the chain, and ! ends the guard.

Emits a.b under a nil check on a temp. The temp runs in front of the statement. Where that would change the result, the chain keeps its temps: after and, or and ??, in a later branch or a guard, in a loop condition, or after an earlier call. There a path of fields reads again, (if a == nil or a.b == nil then nil else a.b.c), and any other chain runs in a closure called in place.

?(

f?(x)

Safe call: nil when f is nil, and x does not evaluate.

?[

a?[k]

Safe index by key: nil when a is nil, else a[k].

...

{ ...base, x = 1 }

Table spread. Emits spread(base, { x = 1 }) from the std. A positional entry after a spread is an error.

[

local xs = [ 1, 2, 3 ]
local grid = [ [1, 2], [3, 4] ]
local [head, ...tail] = xs

An array literal: Array<T> with its metatable, so xs:map(f) works on it. Items separate with , or ;. [[ is Luau's long string, so a nested array puts a space between the brackets: [ [1, 2] ]. In a local or a pattern the bracket destructures, and ...rest takes the tail. $set[ ] and $map[ ] build the other collections.

<<

local atom = charm.atom<<number>>(0)
local m = import<<Module>>(path)
new Stack<<string>>()

Explicit type arguments on a call, a method call, new, or import. Luau reads none at a call, so the emit casts the result instead. A pack generic, Signal.new<T...>, takes none.

<T: Bound>

function largest<T: Ord>(xs: T[]): T
function show<T: Display & Debug>(v: T)

A bound on a generic: the argument must have the trait's methods. & asks for several. A std shape, Display, Eq, Ord, Add, resolves to the runtime's type; a trait the file declares resolves to its own. The bound is the type the parameter carries in the body, and every type the body writes with it carries it too: local best: T? = nil, a loop variable x: T, a cast :: T, and a closure's parameters and return. A closure with a T of its own keeps that one bare. A bound goes on a function or a method: a struct, an enum, or an interface takes none on its parameters, because a Luau type alias checks no bound.

=>

a=>b

Blocking child lookup: a:WaitForChild("b"). With no wait_timeout the call waits forever and cannot return nil, so the result is an Instance. With wait_timeout under [emit] in alloy.toml the call takes that timeout, so it can return nil and => guards the rest of the chain the way -> does. A name that may be nil, such as gui in const gui = player=>PlayerGui, is guarded too: gui=>Hud is nil when gui is. The file alloy init writes sets five seconds; a project that wants the non-optional form removes the key.

~

local v: ~nil = 1

local function label<T: ~nil>(x: T): string
    return tostring(x)
end

Type negation: ~T is every value that is not a T. It reads in a binding, a parameter, a return, a field, a type alias, a union, an intersection, a generic argument, and a bound. ~ binds tighter than | and &, so ~number | string is (~number) | string, and ~(number | string) negates the union.

Luau negates a primitive, a singleton, a class, or a union of them. A table or a function type reports, also as a member of a union or behind an alias, and so does ~~T, which the quick fix rewrites to T. A remote parameter and a field of a struct that derives Serialize take no negation, because a layout names what a value is.

The emit writes __neg<T>, and the first line of code declares it as a type function over types.negationof. The checker enforces it in strict mode and prints it as ~T. Start with ~nil: a negation says what a value is not, and nothing about what it is.