lloyAlloy on GitHub
A strict superset of Luau

Write Alloy.
Ship plain Luau, line for line.

Every Luau file is already an Alloy file. Alloy adds the syntax Luau lacks, compiles each construct to fixed Luau on the same line, and gives the editor a language server that sees Alloy.

emit never adds a linestrict by defaultluau-lsp does the typingRoblox and plain Luau
Operators

Safe access that emits fixed Luau

Nine operators cover the nil checks that Roblox code writes by hand. Each one has a fixed emit. The compiler never looks at a type.

Alloysafe.aly
1local hp = player?.Character?.Humanoid?.Health ?? 100
2local spawn = workspace->Map->Spawns->Spawn1
3local gui = player.PlayerGui=>Hud=>Health
4cache[key] ??= compute(key)
5local root = model.PrimaryPart!
Emitted Luausame line count
1local _1 = (if player == nil then nil else player.Character) local _2 = (if _1 == nil then nil else _1.Humanoid) local _3 = (if _2 == nil then nil else _2.Health) local hp = (if _3 == nil then 100 else _3)
2_1 = (if workspace == nil then nil else workspace:FindFirstChild("Map")) _2 = (if _1 == nil then nil else _1:FindFirstChild("Spawns")) local spawn = (if _2 == nil then nil else _2:FindFirstChild("Spawn1"))
3local gui = player.PlayerGui:WaitForChild("Hud"):WaitForChild("Health")
4if cache[key] == nil then cache[key] = compute(key) end
5local root = (if model.PrimaryPart == nil then error("model.PrimaryPart is nil") else model.PrimaryPart) return nil
Strict by default

The checker runs strict. The compiler holds the rest.

Luau strict mode is the floor. alloy init writes it into .luaurc and .config.luau. The compiler adds five contracts the checker cannot see. Each one is a compile error.

Exhaustive match

A match with no default covers every variant. The error names the one with no arm.

match msg with
    case Join(p) then greet(p)
    case Chat(p, t) then say(p, t)
end
-- `Msg` has no arm for `Leave`; add it or a `default` arm

Struct construction

The fields form sets every field without a default and names no field the struct lacks.

struct P
    x: number
    y: number = 0
end
local a = new P { y = 1 }
-- `new P { ... }` leaves `x` unset

Trait contract

An impl writes every method the trait declares without a body, with the trait's arity.

trait Shape
    function area(self): number
end
impl Shape for Sq
end
-- `impl Shape for Sq` does not write `area`

Sealed structs

A struct is open: a typo in a field name makes a new key in silence. @sealed makes it an error, at runtime and at check time. A `read` field is the one that cannot be written.

@sealed
struct Config
    volume: number
end
local c = new Config { volume = 1 }
c.volume = 2  -- fine: declared
c.volme = 2   -- error: Config has no field volme

Wire types

A remote carries data. A function, a thread, a Future, or a Signal in a parameter is an error.

remote Ping(cb: () -> ()) from client
-- parameter `cb` has type `() -> ()`, which is a
-- function type; a remote carries only data

And the lints

The lints cover nil discipline, unreachable and empty defaults, the legacy scheduler globals, and unused imports. With [lint] strict = true, they also reject an implicit any.

Every lint, in the reference →
The language

Fifteen chapters, each with its emit beside it.

Tooling

One binary, one server, one extension.

The alloy command builds, checks, lints, formats, and documents. The language server runs luau-lsp over a mirror of the compiled files. Hover, completion, and diagnostics land on the Alloy lines. The VS Code extension connects both.

MCPalloy mcp serves these docs to coding agents
Commands
alloy build                          # the project of the nearest alloy.toml
alloy build file.aly                 # one file to stdout; --check for what luau-lsp sees
alloy check                          # compile everything, write nothing, report errors and lints
alloy flux                           # the compile, the type check, and every lint; --fix rewrites
alloy lint                           # the lints alone; -W, -A, -D set a level for the run
alloy fmt                            # format the sources in place; --check writes nothing
alloy test --run                     # a lest spec per source with a @test, then lest; --watch, --coverage
alloy doc strict                     # an article; `alloy doc` lists every topic
alloy mcp                            # the same docs for a coding agent, over MCP
alloy init                           # alloy.toml, .luaurc, and .config.luau
alloy migrate                        # alloy.toml rewritten as .config.aly, with its keys and comments
rojo serve .alloy/build.project.json # the compiled tree, from the [mount] table
alloy self install                   # alloy and alloy-lsp into ~/.alloy/bin; self update fetches a release
Install

From the repository.

The build script compiles every crate and the extension. It then installs the two binaries into ~/.alloy/bin. Add that directory to your PATH. Install the .vsix, and open a folder that holds an .aly file.

Shell
scripts/build.sh --release --install   # alloy and alloy-lsp into ~/.alloy/bin
alloy init                             # alloy.toml, .luaurc, .config.luau
alloy build                            # src/ to build/, plus the runtime
alloy check                            # errors and lints, nothing written
rojo serve .alloy/build.project.json   # the compiled tree, from [mount]