--- alwaysApply: true --- # Luau Language Rules **Default context: Roblox Luau.** Unless otherwise stated, assume we are programming in Roblox Studio. Standard Luau differences are noted where relevant. ## Finding Documentation - `.cursor/rules/roblox/en-us/luau/` - Roblox-specific Luau documentation (types, Roblox classes/enums, Studio integration) - `.cursor/rules/luau/` - Official Luau language documentation (type system, standard library, syntax, guides) - `.cursor/rules/luau-rfcs/` - RFC documents detailing every language feature and design decision ## Single-Threaded Execution Model **Luau is completely single-threaded.** This is the most misunderstood aspect of the language. ### Key Facts - Code in one script **cannot** run simultaneously with another script - Execution only switches when code **yields** (`task.wait()`, `:Wait()`, `coroutine.yield()`, async APIs) - **No yield = no other code runs.** Infinite loops without yields freeze the game. ### Practical Implications ```luau -- SAFE: No yield between read/write, guaranteed atomic local value = data.Value data.Value = value + 1 -- UNSAFE: Yield allows other code to modify data.Value local value = data.Value task.wait(0.1) -- Other code can run here! data.Value = value + 1 -- May not be what you expect ``` ### Common Misconceptions 1. **"Multiple scripts run in parallel"** - False. They switch only at yield points. 2. **"I need mutexes/locks"** - Not in pure Luau. No yield = exclusive access. 3. **"`task.spawn` runs code in parallel"** - False. It schedules for later. ## Luau-Specific Syntax ### Arrays Are 1-Indexed **Critical:** Unlike most languages, Luau arrays start at index 1, not 0. ```luau local arr = {"first", "second", "third"} print(arr[1]) -- "first" (NOT arr[0]) print(arr[0]) -- nil for i = 1, #arr do -- Start at 1 print(arr[i]) end ``` ### Generic Instantiation Uses `<>` This is unique to Luau and easy to forget: ```luau -- Correct: Double angle brackets local binding = React.createBinding<>() local result = someFunc<>() -- WRONG: Single angle brackets (won't work) -- local binding = React.createBinding() ``` ### Type Ascription Uses `::` ```luau local x = someValue :: string local y = (a + b) :: number ``` ### String Interpolation Uses Backticks ```luau local name = "world" print(`Hello {name}!`) -- Hello world! print(`Result: {1 + 2}`) -- Result: 3 -- Escape with backslash print(`Literal braces: \{not interpolated}`) ``` ### String Literal Escapes Beyond standard escapes, Luau supports: ```luau "\xAB" -- Hex escape: inserts byte 0xAB "\u{1F600}" -- Unicode escape: inserts UTF-8 for U+1F600 (braces required) "long\z string" -- \z skips following whitespace/newlines: "longstring" ``` Backtick strings (interpolated) use `\` to escape `` ` ``, `{`, `\`, and newlines. `{{` is a parse error (use `\{` to get a literal brace). ### If Expressions (Ternary) ```luau local x = if condition then value else alternative local size = if x > 10 then "big" elseif x > 5 then "medium" else "small" ``` ### Continue Statement ```luau for i = 1, 10 do if i % 2 == 0 then continue end print(i) end ``` ### Compound Assignment ```luau a += b -- a = a + b x //= 2 -- Floor division str ..= "suffix" ``` ### Floor Division ```luau local half = width // 2 -- More efficient than math.floor(width / 2) ``` ### Number Literals Luau has a single number type (64-bit IEEE 754 double, integers exact up to 2^53). ```luau local hex = 0xFF -- Hexadecimal local bin = 0b10101010 -- Binary local big = 1_048_576 -- Decimal separators for readability local hexSep = 0xFFFF_FFFF local binSep = 0b_0101_0101 ``` ## Type System Essentials ### Type Modes - `--!strict`: Full type checking (recommended) - `--!nonstrict`: Reduced checking - `--!nocheck`: Disabled ### Primitive Types The Luau VM has 10 primitive types: `nil`, `string`, `number`, `boolean`, `table`, `function`, `thread`, `userdata`, `vector`, `buffer`. ```luau local x: number local s: string local b: boolean local co: thread = coroutine.running() local buf: buffer = buffer.create(256) ``` Note: `table` and `function` use dedicated syntax (not written by name). `vector` is not representable by name in type annotations. `userdata` is represented by concrete host types. ### `unknown`, `any`, and `never` ```luau local u: unknown = anything -- Top type: accepts all values, but cannot be used without refinement local a: any = anything -- Escape hatch: opts out of type checking entirely local n: never -- Bottom type: no value inhabits this type ``` - `unknown` requires type refinement (e.g., `typeof(x) == "string"`) before use - `any` silently allows all operations -- use sparingly - `never` is useful for exhaustiveness checking and impossible branches ### Core Type Syntax ```luau local arr: {string} -- Array shorthand (equivalent to {[number]: string}) local map: {[string]: number} -- Dictionary with indexer local obj: { x: number, y: number } -- Table with named properties local maybe: string? -- Optional: string | nil -- Union / Intersection (leading | and & are allowed for formatting) type Result = "ok" | "error" type Action = { type: "add", value: number } | { type: "remove", id: string } type Combined = TypeA & TypeB type NotNil = ~nil -- Negation type -- Singleton (literal) types -- strings and booleans only, not numbers type Direction = "up" | "down" | "left" | "right" type Truthy = true -- Function types with optional named parameters type Callback = (number, string) -> boolean type Named = (errorCode: number, errorText: string) -> () type Generic = (T) -> T type Multi = (number, string) -> (boolean, string) -- Multiple returns ``` ### Generics with Defaults Type aliases support default type parameters (functions do not): ```luau type Pair = { first: T, second: T } type PairWithDefault = Pair local p: PairWithDefault = { first = "hello", second = "world" } -- T defaults to string ``` ### Read-Only Properties ```luau type ReadOnly = { read x: number } type WriteOnly = { write x: number } ``` ### Tagged Unions (Discriminated Unions) Common pattern for type-safe variants: ```luau type Action = | { type: "increment" } | { type: "decrement" } | { type: "set", value: number } function reducer(state: number, action: Action): number if action.type == "increment" then return state + 1 elseif action.type == "decrement" then return state - 1 elseif action.type == "set" then return action.value else error(`Unknown action`) end end ``` ### Built-in Type Functions ```luau type Keys = keyof -- Union of all property names type RawKeys = rawkeyof -- Keys without __index type PropType = index -- Property type lookup type RawProp = rawget -- Property lookup without __index ``` ### Variadic Type Packs For variadic functions, use `...type` in type definitions: ```luau -- Function definition local function addNumbers(...: number) -- implementation end -- Type definition type AddNumbers = (...number) -> number -- Generic type packs (advanced) type Fields = { call: (self, P...) -> R... } ``` ### Type Exports Share types across modules with `export type`: ```luau -- In Types.luau export type Player = { name: string, score: number } -- In another module local Types = require(ReplicatedStorage.Types) local player: Types.Player = {name = "Alice", score = 100} ``` ### Inferred Types with `typeof` Use `typeof` to infer types from values: ```luau local config = { maxRetries = 3, timeout = 30 } type Config = typeof(config) -- Infers {maxRetries: number, timeout: number} -- Useful for metatable types type Vector = typeof(setmetatable({}::{ x: number, y: number }, {}::{ __add: (Vector, Vector) -> Vector })) ``` ### Type Refinements The type checker narrows types based on control flow: ```luau local x: string | number = getValue() -- Type guard with type() or typeof() if type(x) == "string" then local s: string = x -- ok, narrowed to string end -- Truthy test local maybe: string? = getMaybe() if maybe then local s: string = maybe -- ok, nil ruled out end -- Equality narrowing to singleton local s: string = getStr() if s == "hello" then local h: "hello" = s -- ok, narrowed to literal end -- Compose with and/or/not if (type(x) == "string" or type(x) == "number") and x ~= nil then -- x: string | number end -- assert() also refines assert(type(x) == "string") local s: string = x -- ok after assert ``` ### Sealed vs Unsealed Tables Tables have different mutability rules depending on how they are created: **Unsealed** (table literals without annotation) -- can add new properties until scope exits: ```luau local t = { x = 1 } -- unsealed: {x: number} t.y = 2 -- ok, adds y: {x: number, y: number} t.z = 3 -- ok local u: { x: number, y: number? } = t -- ok, y is optional ``` **Sealed** (annotated or returned from function) -- locked, supports width subtyping: ```luau local t: { x: number } = { x = 1 } t.y = 2 -- NOT ok, table is sealed type Point2D = { x: number, y: number } type Point1D = { x: number } local p: Point2D = { x = 5, y = 37 } local q: Point1D = p -- ok, Point2D is a subtype (has extra properties) ``` Once an unsealed table leaves its defining scope (e.g., returned from a function), it becomes sealed. ### User-Defined Type Functions Type functions run at analysis time and operate on types using the `types` library: ```luau type function simple_keyof(ty) if not ty:is("table") then error("Can only call keyof on tables.") end local union = nil for property in ty:properties() do union = if union then types.unionof(union, property) else property end return if union then union else types.singleton(nil) end type Person = { name: string, age: number } type Keys = simple_keyof -- "age" | "name" ``` Available in type function environment: `types` library, `assert`, `error`, `print`, `next`, `pairs`, `ipairs`, `select`, `unpack`, `tostring`, `tonumber`, `type`, `typeof`, `math`, `table`, `string`, `bit32`, `utf8`, `buffer` libraries. ## Roblox-Specific For full Roblox Luau documentation, see `.cursor/rules/roblox/en-us/luau/`. ### Roblox Types in Annotations All Roblox classes, data types, and enums are valid type annotations: ```luau local part: Part = Instance.new("Part") local color: BrickColor = part.BrickColor local material: Enum.Material = part.Material local pos: Vector3 = part.Position local cf: CFrame = part.CFrame ``` ### Use Vector3, Not vector The `vector` primitive exists but is not compatible with Roblox APIs. Always use `Vector3`: ```luau local pos = Vector3.new(1, 2, 3) -- NOT: vector.create(1, 2, 3) ``` ### Native Code Generation (Server Only) In Roblox, `--!native` compiles **server-side scripts** to machine code. There is a limit on total natively compiled code per experience. Use `@native` on individual hot functions to stay within budget. ## Module Require Semantics ### Roblox Luau (Default) In Roblox, use instance-path requires. This is the preferred style: ```luau local ReplicatedStorage = game:GetService("ReplicatedStorage") local MyModule = require(ReplicatedStorage.Modules.MyModule) local Types = require(ReplicatedStorage.Shared.Types) -- Common pattern: services at top of file local Players = game:GetService("Players") local RunService = game:GetService("RunService") local ServerStorage = game:GetService("ServerStorage") ``` ### Standalone Luau (String Requires) Outside Roblox (e.g., Lune, standalone tooling), requires use explicit string paths with mandatory prefixes: ```luau local sibling = require("./sibling") -- Relative to current file local parent = require("../parent") -- Parent directory local lib = require("@lune/net") -- Alias (configured in .luaurc) ``` All paths must start with `./`, `../`, or `@` -- bare names like `require("foo")` are not allowed. Aliases are configured in `.luaurc`: ```json { "aliases": { "lune": "packages/lune", "shared": "src/shared" } } ``` ### Cyclic Dependencies Require paths must be statically resolvable. To break cyclic module dependencies, cast to `any`: ```luau local myModule = require(MyModule) :: any ``` ## Deprecated Functions **Never use these:** - `getfenv`/`setfenv`: Breaks optimizations and type checking - `table.getn`: Use `#` or `rawlen` - `table.foreach`/`table.foreachi`: Use `for..in` loops - `wait()`: Use `task.wait()` ## Common Gotchas ### Raw Table Functions When working with metatables, use `rawget`/`rawset`/`rawequal` to bypass metamethods: ```luau local t = setmetatable({}, { __index = function(self, key) rawset(self, key, key * 10) -- Avoid triggering __newindex return self[key] end }) -- rawget: Get without triggering __index local val = rawget(t, "key") -- rawset: Set without triggering __newindex rawset(t, "key", "value") -- rawequal: Compare without triggering __eq if rawequal(a, b) then end ``` ### Metamethod Differences from Lua - `__eq` always called, even for rawequal objects - `__len` works on tables (not just userdata) - `__iter` enables custom iteration ### No Bitwise Shift Operators `<<` and `>>` don't exist (conflicts with generic syntax). Use `bit32.lshift`/`bit32.rshift`. ### Table Iteration Tables can be iterated directly without `pairs`/`ipairs`: ```luau for k, v in myTable do -- Works directly end ``` ### Spreading Arrays with `unpack` Use `unpack()` to spread array values: ```luau local args = {1, 2, 3} print(unpack(args)) -- 1 2 3 local function add(a, b, c) return a + b + c end print(add(unpack(args))) -- 6 ``` ### Function Attributes ```luau @native function hotFunction() -- Compiled to native code end @deprecated function oldFunction() end ``` Note: `@inline` is NOT supported. Compiler handles inlining automatically. ## OOP Pattern Separate the data type from the class type using `typeof(setmetatable(...))`. Annotate `self` explicitly on methods -- this is **required** for the type checker to share `self` types across methods. ```luau local Point = {} Point.__index = Point type PointData = { x: number, y: number, } export type Point = typeof(setmetatable({} :: PointData, Point)) function Point.new(x: number, y: number): Point return setmetatable({ x = x, y = y }, Point) end function Point.move(self: Point, dx: number, dy: number) self.x += dx self.y += dy end function Point.magnitude(self: Point): number return math.sqrt(self.x * self.x + self.y * self.y) end local p = Point.new(3, 4) p:move(1, 2) -- Callers still use : syntax print(p:magnitude()) -- 5.385... ``` Key points: - Define methods with `.` syntax and explicit `self: ClassName` parameter - Callers still invoke with `:` syntax - Return type annotation on the constructor ensures compatibility with methods - The `typeof(setmetatable(...))` trick gives full autocomplete on `self` ## Performance Tips 1. **Preallocate tables**: `table.create(count)` for known sizes 2. **Reuse tables**: `table.clear()` instead of creating new 3. **Floor division**: `//` is faster than `math.floor(a / b)` 4. **Avoid yields in hot paths**: Remember the single-threaded model ### Native Code Generation Use `--!native` to compile server scripts to machine code for ~2-10x speedup: ```luau --!native --!strict -- Best for: computation-heavy code, math operations, buffer manipulation -- Avoid: getfenv/setfenv, non-numeric args to math functions ``` Or use `@native` for individual functions: ```luau @native local function calculatePhysics(dt: number) -- Heavy computation here end ``` **When to use:** - Functions with heavy math/loops called frequently - `buffer` operations - Avoid for API-heavy code (gains are minimal) **Annotate `Vector3` parameters** for best results: ```luau -- Slow: "v" assumed to be table local function magnitude(v) return math.sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z) end -- Fast: compiler generates optimized vector code local function magnitude(v: Vector3) return math.sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z) end ``` ## Module Exports with Types ```luau --!strict export type MyType = { field: string } local Module = {} function Module.create(): MyType return { field = "value" } end return Module ``` ## Exhaustiveness Checking Use `never` type to ensure all cases are handled: ```luau function handle(x: "a" | "b") if x == "a" then return 1 elseif x == "b" then return 2 else local _: never = x -- Type error if not exhaustive end end ``` ## Standard Library Additions Luau extends Lua 5.1's standard library with many functions. Below are the most important additions not covered elsewhere. ### Math Library Extensions ```luau math.clamp(n, min, max) -- Clamp n to [min, max] range math.sign(n) -- Returns -1, 0, or 1 math.round(n) -- Round to nearest integer (halfway rounds away from 0) math.lerp(a, b, t) -- Linear interpolation: a + (b - a) * t, exact at t=1 math.map(x, inmin, inmax, outmin, outmax) -- Map x from input range to output range math.noise(x, y?, z?) -- 3D Perlin noise, returns [-1, 1] ``` Also available: `math.log(n, base?)` (optional base), `math.random()` uses PCG RNG. ### String Library Extensions ```luau -- string.split: splits string into array (default separator: ",") local parts = string.split("a,b,c", ",") -- {"a", "b", "c"} local chars = string.split("hello", "") -- {"h", "e", "l", "l", "o"} -- string.pack/unpack/packsize: binary data encoding (Lua 5.3 format strings) -- Fixed sizes: short=16bit, long=64bit, int=32bit, size_t=32bit local packed = string.pack("i4f", 42, 3.14) local int, float = string.unpack("i4f", packed) local size = string.packsize("i4f") -- 8 bytes ``` ### bit32 Library Extensions ```luau bit32.countlz(n) -- Count leading zeros (returns 32 if n is 0) bit32.countrz(n) -- Count trailing zeros (returns 32 if n is 0) bit32.byteswap(n) -- Swap byte order (endianness) ``` ### coroutine.close ```luau coroutine.close(co) -- Puts suspended/dead coroutine into dead state, frees stack -- Returns (true) on success, (false, error) if coroutine was in error state ``` ## Immutable Tables ```luau local config = table.freeze({ maxRetries = 3, timeout = 30 }) -- Clone before modifying local newConfig = table.clone(config) newConfig.maxRetries = 5 ``` ## Buffer Library Buffers are fixed-size mutable byte arrays. All offsets are 0-based. Reads/writes outside bounds throw errors. Uses little-endian byte order. ```luau local buf = buffer.create(1024) -- All bytes initialized to 0 local buf2 = buffer.fromstring("hello") -- From string contents local str = buffer.tostring(buf2) -- Back to string local len = buffer.len(buf) -- Size in bytes -- Typed reads/writes (i=signed, u=unsigned, f=float) buffer.writei32(buf, 0, 42) -- Write signed 32-bit int at offset 0 local n = buffer.readi32(buf, 0) -- Read it back buffer.writef64(buf, 4, 3.14159) -- Write 64-bit float at offset 4 local f = buffer.readf64(buf, 4) -- Available: readi8/u8/i16/u16/i32/u32/f32/f64 and matching write variants -- String read/write buffer.writestring(buf, 0, "hello", 5) -- Write 5 bytes from string local s = buffer.readstring(buf, 0, 5) -- Read 5 bytes as string -- Bit-level access buffer.writebits(buf, 0, 4, 0b1010) -- Write 4 bits at bit offset 0 local bits = buffer.readbits(buf, 0, 4) -- Read 4 bits (returns unsigned int) -- Bulk operations buffer.copy(target, tOff, source, sOff?, count?) -- Copy between buffers (or within same) buffer.fill(buf, offset, value, count?) -- Fill bytes with value ``` ## Vector Library The `vector` primitive is a 3-component (or 4-component in 4-wide mode) immutable value type. Components are accessed via `.x`/`.y`/`.z` (and `.w` in 4-wide mode) but cannot be written individually. ```luau -- Construction and constants local v = vector.create(1, 2, 3) local zero = vector.zero -- (0, 0, 0) local one = vector.one -- (1, 1, 1) -- Arithmetic: +, -, *, /, //, unary - all work component-wise local sum = v + vector.create(4, 5, 6) -- Geometric operations vector.magnitude(v) -- Length of vector vector.normalize(v) -- Unit vector vector.dot(v1, v2) -- Dot product (returns number) vector.cross(v1, v2) -- Cross product (3D only) vector.angle(v1, v2, axis?) -- Angle in radians (axis for signed angle) vector.lerp(v1, v2, t) -- Linear interpolation -- Component-wise math vector.abs(v) -- math.abs per component vector.sign(v) -- math.sign per component vector.floor(v) / vector.ceil(v) -- Rounding per component vector.clamp(v, min, max) -- Clamp per component vector.max(v1, v2, ...) / vector.min(v1, v2, ...) ``` **Note:** In Roblox, prefer `Vector3` over `vector` -- `Vector3` integrates with all Roblox APIs. The `vector` library is for standalone Luau or performance-critical native code. ## Differences from Lua Luau is based on Lua 5.1 with selective features from 5.2-5.4 and its own additions: - `_VERSION` is `"Luau"` (not `"Lua 5.1"`) - Single number type: 64-bit double (no separate integer type) - No tail call optimization (simplifies debugging and stack traces) - `__eq` metamethod is always called, even when objects are rawequal - No `goto` statement - No bitwise operators (`<<`, `>>`, `&`, `|`, `~`) -- use `bit32` library - No `__gc` metamethod (sandboxing constraint) - No `io`, `os.execute`, `package`, or full `debug` library (sandboxing) - `loadstring` cannot load bytecode, `string.dump` not available - Table assignment order in literals follows program order (Lua 5.x may reorder) - `function()` expressions may reuse closures when all captured upvalues are identical - `os.time` with table argument returns UTC (not local time)