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
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.
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.
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!
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
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.
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
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
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`
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
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
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.
Operators·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.
requireModules·Paths pass through unchanged. A Luau alias, a relative path, or a package works the way it already does. Type-only imports cost nothing at runtime.
async functions return a FutureConcurrency·The body runs on task.spawn under xpcall, and the Future memoizes the result. await yields until it settles. try await turns a failure into a Result.
match compiles to an if-chainData·A payload enum is a tagged table with a constructor per variant and a union type the analyzer can narrow. A unit enum is a string literal union, so it serializes for free.
Control flow·if local, elseif local, while local, and the expression form bind a value and test it in one step. The branch declares the name from the tested temp. Inside the branch it is T, not T?, and closures see the same.
Types·An Alloy struct is a plain table that vanilla Luau can use. Fields carry defaults and read or write marks. impl adds methods. A trait is a type of signatures plus default bodies.
Types·interface is sugar over type with extends. Partial, Readonly, and Sink are language-level types. Type functions that the real solver runs build them, and a file may replace any of them with its own.
Sugar·A ternary, an object initializer, array destructuring, a method reference, and delete with runtime dispatch. Every form keeps its line.
Types·impl Vector3 or impl string adds methods to a type you do not own. The server injects each method into the analyzer's definitions. The call types, completes, and hovers like a built-in.
$ sigilCompile time·Intrinsics see source text and position. A macro is a compile-time template with expression parameters. Its declaration leaves blank lines, and each call expands in place.
Metadata·Built-in attributes drive codegen. Declared ones are metadata with typed arguments, and Attributes reads them at runtime. Completion after @ lists only what fits the position.
Networking·One declaration names the parameters, their wire widths, and the side that fires or asks. The compiler generates the serializer. fire, fire_all, on, and call are the whole surface.
UI·An .alx file is luaux markup with Alloy inside the holes. Tags, attributes, fragments, and comments lower to the configured factory. The language server answers hover and completion for tags and attributes itself.
Testing·A @test function runs under alloy test. The ship artifact blanks it line for line. The check artifact keeps it typed, so the editor still sees it.
parallel blocks, and typed messagesConcurrency·A file named worker.server.actor.aly builds into an Actor. A parallel do block runs in the parallel phase, and the compiler refuses the writes the engine refuses. A message types both ends of SendMessage, and its reply comes back to any script.
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.
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
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.
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]