Luau vs Lua 5.1 — Complete Difference Reference
Luau version covered: 0.730 (latest tagged release, 17 July 2026) Baseline: Lua 5.1.5 Sources: luau.org docs, luau-lang/luau release notes (0.720–0.730), luau-lang/rfcs
Luau is syntactically backwards compatible with Lua 5.1 — any valid Lua 5.1 program is a valid Luau program, with the caveat that some 5.1 library functions have been removed for sandboxing. Luau is not a superset of later Lua versions; it cherry-picks from 5.2–5.5 and adds a large amount of its own.
In the AntOS wiki: this is the exhaustive upstream reference. For the AntOS-oriented introduction start at the Luau hub and The Luau language; for hands-on porting see the migration tutorials. Note that the sandboxing removals in §6 (
io, most ofos) are re-supplied by AntOS through its ownio/os/fslibraries.
Status markers used below:
| Marker | Meaning |
|---|---|
| ✅ | Shipped and stable |
| 🚧 | RFC accepted, implementation landing now (0.72x) — verify against your build |
| ⚠️ | Behavioural difference that can silently break ported Lua code |
Table of contents
- The 30-second summary
- Syntax additions
- The type system
- New value types
- Standard library differences
- Removed from Lua 5.1 (sandboxing)
- Behavioural / semantic differences
- Implementation limits
- Lua 5.2 / 5.3 / 5.4 / 5.5 feature status
- C API and embedding differences
- Modules, require, and .luaurc
- Tooling
- Recent changes: 0.720 → 0.730
- Porting checklist
1. The 30-second summary
What you gain over Lua 5.1:
- A full gradual type system with inference, generics, unions/intersections, refinements and user-defined type functions.
continue, compound assignment,//,if..then..elseexpressions, string interpolation, generalizedfor..initeration,constbindings,export, function attributes.- New value types:
vector(native, SIMD-friendly),buffer(fixed-size mutable memory), and 🚧integer(true 64-bit). - A much faster VM, native code generation (
@native/--!native), and a bytecode inliner. - Real sandboxing (
table.freeze, read-only globals,luaL_sandbox).
What you lose:
io,package, most ofosanddebug,loadstring/loadfile/dofile,string.dump.- Tail calls (⚠️ deep recursion will overflow).
__gc,goto,<close>,<const>attribute syntax, bitwise operators, 64-bit integer operators.
2. Syntax additions
2.1 String literal escapes ✅
Luau adopts the Lua 5.3 escape set on top of 5.1:
local hex = "\x41" -- "A"
local uni = "\u{1F600}" -- UTF-8 bytes for U+1F600 (braces are mandatory)
local long = "this is a very \z
long literal" -- \z eats the newline + following whitespace
2.2 Number literals ✅
local dec = 1048576
local hex = 0xFFFF -- also in Lua 5.1
local bin = 0b0101_0101 -- Luau only
local sep = 1_048_576 -- underscore separators, Luau only
local both = 0xFFFF_FFFF
Numbers are still a single type: 64-bit IEEE-754 doubles (exact integers to 2^53). Literals larger than that lose precision — unless you use the new integer suffix (§4.3).
2.3 continue ✅
Lua 5.1 has no continue (and Luau deliberately rejects goto, so the ::continue:: workaround
doesn't apply either).
for i = 1, 10 do
if i % 2 == 0 then
continue -- must be the last statement in its block
end
print(i)
end
continue is a contextual keyword, not a reserved word, so continue() still parses as a
function call and old code using continue as an identifier keeps working.
Restriction: inside repeat..until, continue may not skip the declaration of a local that the
until condition reads — that's a compile error.
repeat
continue
local x = false -- error: continue would skip this declaration
until x
2.4 Compound assignment ✅
local n = 1
n += 1 -- also -= *= /= //= %= ^= ..=
local t = { count = 0 }
t.count += 5
They are statements, not expressions (no local a = (b += 1)). The left-hand side is evaluated
once, so getTable().field += 1 calls getTable() a single time — unlike the manual expansion.
Arithmetic and index metamethods (__add, __index, __newindex, …) are honoured, so custom
types work with no extra effort.
2.5 Floor division // ✅
Taken from Lua 5.3, with //= and an __idiv metamethod.
print(7 // 2) --> 3
print(-7 // 2) --> -4 (rounds toward negative infinity)
print(7 // 0) --> inf
print(0 // 0) --> nan
For the vector type, v // n applies math.floor component-wise.
2.6 if..then..else expressions ✅
local maxValue = if a > b then a else b
local size =
if n < 10 then "small"
elseif n < 100 then "medium"
else "large"
else is mandatory. This is the recommended replacement for the a and b or c idiom, which
misbehaves when b is false or nil:
local wrong = cond and false or true -- always true!
local right = if cond then false else true
2.7 String interpolation ✅
local name, count = "world", 3
print(`hello {name}, you have {count} item{if count == 1 then "" else "s"}`)
print(`sum = {1 + 2}, nested = {("x"):rep(3)}`)
Rules and gotchas:
- Inside backticks,
\escapes`,{,\and newline. {{is a parse error (deliberately, to stop people writing JS/C#-style escapes).- Backtick strings are not allowed in type annotations:
type Foo = `Foo`is invalid. - Unlike
"..."and'...', a backtick string cannot be used as a bare call argument —fx`is invalid, writef(x)`.
2.8 Generalized iteration ✅
In Lua 5.1 you must call pairs/ipairs. In Luau you can iterate a table directly:
local t = { a = 1, b = 2 }
for k, v in t do -- no pairs() needed
print(k, v)
end
Extendable to tables and userdata via the __iter metamethod, which is called once before
iteration begins and returns an iterator function:
local Range = {}
Range.__index = Range
function Range.__iter(self)
local i = self.from - 1
return function()
i += 1
if i <= self.to then return i end
end
end
local r = setmetatable({ from = 1, to = 3 }, Range)
for n in r do print(n) end --> 1 2 3
Ordering guarantees: elements 1..#t are visited consecutively, the rest in unspecified order.
Mutating keys other than the current one is unspecified — with one exception: in a dense array,
non-nil elements appended at #t+1 during the loop are guaranteed to be visited.
__pairs / __ipairs from Lua 5.2 are not supported and never will be; __iter supersedes
them.
New in 0.722: the iterator function may now yield. (Metamethods — including __iter
itself — still may not yield.)
-- the generator can now block on IO and yield
for request in net.serve(8080) do
request.respondWith("Echo: " .. request.body)
end
2.9 const bindings ✅
Luau's answer to Lua 5.4's <const>, with syntax it considers less ugly. const is a contextual
keyword valid anywhere local is.
const MAX = 100
MAX = 200 -- error: cannot reassign a const binding
MAX += 1 -- also an error
const x: number = 5 -- type annotations work
const a, b = 1, 2 -- multi-assignment works
const function f() end -- function declarations work
⚠️ const freezes the binding, not the value:
const t = { count = 0 }
t.count += 1 -- fine, the table is still mutable
t = {} -- error
const frozen = table.freeze({ count = 0 })
frozen.count = 1 -- now this errors too
Backwards compatible: local const = 5 still works, because const is only a keyword where
local would be valid.
2.10 export for values ✅ (0.723)
Historically Luau only had export type. RFC 179 added value exports at module top level.
-- mylib.luau
export local version = "5.1"
export const TAU = math.pi * 2
export function init() -- exported functions are always const
-- ...
end
export type Point = { x: number, y: number }
if foo then
export local bar = 1 -- syntax error: top level only
end
A module with exports returns a frozen table of them. Exported bindings count toward the local
variable limit, since the compiler can lower them to locals. This is what makes cross-module
inlining and constant folding possible — the reason const was introduced alongside it.
2.11 Function attributes ✅
Not in Lua at all. @name annotations before a function declaration, built into the language
(users cannot define new ones).
@native
local function hot(a: number, b: number) -- request native code generation
return a * b + a
end
@deprecated
function oldApi() end
@[deprecated { use = "newApi()", reason = "oldApi leaks handles." }]
function oldApi2() end
@[native, deprecated] -- group multiple in one @[ ]
function both() end
@nativeis the per-function equivalent of the--!nativefile directive. It does not apply recursively to inner functions — mark those separately.@deprecatedmakes the linter warn at call sites and the LSP style the entry differently in autocomplete. As of 0.730 the deprecation is propagated into autocomplete entries.@attr,@[attr]and@[attr()]are all equivalent. For a single string/table parameter the parens are optional.
2.12 Classes 🚧 (RFC accepted, implementation started in 0.721)
The biggest language addition in flight. class and public are new contextual keywords.
class Point
public x: number
public y -- un-annotated fields are typed `any`
function length(self)
return math.sqrt(self.x * self.x + self.y * self.y)
end
function __add(self, other: Point)
return Point { x = self.x + other.x, y = self.y + other.y }
end
function __tostring(self)
return `Point \{ x = {self.x}, y = {self.y} \}`
end
function new(x, y) -- no `self` ⇒ static, call as Point.new(...)
return Point { x = x, y = y }
end
end
local p = Point.new(3, 4)
print(`{p} length = {p:length()}`)
Key semantics:
- Top-level declarations only;
export class Xis allowed; duplicate names in a module are an error. - Instantiate by calling the class object with a table of initial field values:
Point { x = 1, y = 2 }.MyClass(original)shallow-clones. - The class binding is hoisted (so mutually-recursive classes/functions work), but the class object is only allocated when the statement executes.
- Class objects are always
constand frozen. - Instances are a new VM value type, not tables: no array part, no
pairs/ipairs, nogetmetatable/setmetatable, and reading or writing a nonexistent property raises. type()/typeof()return"class"for class objects and"object"for instances.- Class types are nominal, unlike Luau's structural table types.
- Only a fixed metamethod set is allowed (
__add,__sub,__mul,__div,__mod,__pow,__idiv,__unm,__concat,__len,__eq,__lt,__le,__call,__iter,__tostring). Any other__-prefixed method is a syntax error. - New global library:
if class.isinstance(p, Point) then -- participates in type refinement
print(p.x, p.y)
end
local C = class.classof(p) -- class?
Out of scope for now: inheritance, generic classes, private/const fields.
Related cleanup: the old declare class definition-file syntax is being removed in favour of
declare extern type Foo with ... end. extern class was disallowed in 0.727.
3. The type system
Lua 5.1 has none of this. Types are erased at compile time and cost nothing at runtime.
3.1 Modes
--!strict -- full inference, errors on mismatch
--!nonstrict -- default; unknown types become `any`
--!nocheck -- inference disabled for this file
Extra file directives: --!native (native codegen for the whole script),
--!optimize N, --!nolint.
3.2 Annotations and casts
local n: number = 1
local s: string? = nil -- shorthand for `string | nil`
local function add(a: number, b: number): number
return a + b
end
local function none(): () end -- zero returns: parenthesise
local function two(): (number, string) end
local cb: (value: number) -> string -- argument names are documentation only
local names = {} :: { string } -- type cast with ::
local x = value :: any
Casts are themselves checked: one side must be a subtype of the other, or any. Casting a
multiple-return expression keeps only the first value.
3.3 Type aliases, typeof, export type
type Point = { x: number, y: number }
type Vector = typeof(vector.create(0, 0, 0)) -- `typeof` never evaluates its argument
type Handler = (Point) -> ()
export type Config = { retries: number } -- visible to other modules
-- consumer.luau
local lib = require("./lib")
local c: lib.Config = { retries = 3 }
3.4 Tables: arrays, indexers, read/write properties
type Names = { string } -- array-like shorthand
type Lookup = { [string]: number } -- indexer
type Mixed = { n: number, [string]: any }
type ReadOnly = { read x: number } -- property can be read but not written
type WriteOnly = { write y: number }
Read-only indexers ✅ (new in 0.721) — {read T} / {read [K]: V}. This matters because
mutable arrays must be checked invariantly, whereas read-only ones can be checked covariantly:
local function printThemOld(a: { Instance }) end
local function printThemNew(a: { read Instance }) end
local players: { Player } = ...
printThemOld(players) -- rejected: the function could insert a non-Player
printThemNew(players) -- accepted: the function cannot write
3.5 Unions, intersections, singletons
type Result = { ok: true, value: string } | { ok: false, err: string }
type Overloaded = ((number) -> string) & ((boolean) -> string)
type Direction = "north" | "south" | "east" | "west" -- string singletons
3.6 Generics and variadics
local function identity<T>(x: T): T
return x
end
type Map<K, V> = { [K]: V }
local function pack<T...>(...: T...): () -> T...
local args = table.pack(...)
return function(): T...
return table.unpack(args, 1, args.n)
end
end
3.7 Refinements
Control flow narrows types automatically:
local function f(x: string | number)
if type(x) == "string" then
print(x:upper()) -- x : string here
else
print(x + 1) -- x : number here
end
end
local function g(v: Part?)
if not v then return end
print(v.Name) -- v : Part
end
typeof(), type(), assert, truthiness tests and 🚧 class.isinstance all participate.
3.8 User-defined type functions
Type-level computation written in Luau itself:
type function MakeOptional(t)
local props = {}
for k, v in t:properties() do
props[k] = types.optional(v)
end
return types.newtable(props)
end
type PartialPoint = MakeOptional<{ x: number, y: number }>
The types library gained type:issubtypeof in 0.724. types.integer is added by the integer RFC.
3.9 The new solver
Luau has been migrating to a rewritten constraint-based inference engine ("new type solver", DCR). Since 0.721 you can pick explicitly:
luau-analyze --solver=new file.luau # default
luau-analyze --solver=old file.luau
Most 0.72x release notes are new-solver bug fixes; if you're on an older Luau, inference quality differs noticeably.
4. New value types
type() in Luau can return: "nil", "boolean", "number", "vector", "string", "table",
"function", "userdata", "thread", "buffer" — and 🚧 "integer", "class", "object".
Lua 5.1 returns only the first-listed subset (no vector, buffer, integer, class, object).
typeof() is a Luau-only global: for host-defined userdata with a __type metafield it returns
that name; for newproxy userdata it always returns "userdata" so host types cannot be spoofed.
4.1 vector ✅
A native 3-component (or 4 with LUA_VECTOR_SIZE=4) float vector stored inline in the value —
no allocation, no GC pressure. Full operator support.
local a = vector.create(1, 2, 3)
local b = vector.one
local c = a + b * 2
print(c.x, c.y, c.z) -- also .X/.Y/.Z; components are immutable
print(vector.magnitude(a), vector.dot(a, b))
local n = vector.normalize(a)
local x = vector.cross(a, b)
local m = vector.min(a, b, c)
Also: vector.zero, vector.angle, vector.floor, vector.ceil, vector.abs, vector.sign,
vector.clamp, vector.max. Recent builds can be compiled with double-precision vectors
(heap-allocated in that mode).
4.2 buffer ✅
Fixed-size mutable byte block — the thing Lua 5.1 completely lacks. Little-endian, bounds-checked (out-of-range access throws), max 1 GB.
local b = buffer.create(16)
buffer.writeu32(b, 0, 0xDEADBEEF)
buffer.writef32(b, 4, 1.5)
buffer.writestring(b, 8, "abcd")
print(buffer.readu32(b, 0), buffer.len(b))
print(buffer.readstring(b, 8, 4))
buffer.writebits(b, 96, 3, 5) -- bit-level access, 0..32 bits
print(buffer.readbits(b, 96, 3))
buffer.fill(b, 0, 0, 4)
buffer.copy(dst, 0, b, 0, 8) -- overlap-safe
local s = buffer.tostring(b)
local c = buffer.fromstring("hi")
Read/write functions: readi8/u8/i16/u16/i32/u32/f32/f64 and matching write*.
4.3 integer — real 64-bit integers 🚧
RFC merged; the library is referenced in 0.728 release notes. This is Luau's answer to Lua 5.3's integer subtype, done differently: a separate type with no arithmetic operators.
local a = 123i
local b = 1_000i
local c = 0xABABi
local d = 0b1000_1000i
local e = 0xFFFF_FFFF_FFFF_FFFFi -- == -1i
local f = -123i -- unary minus on the literal is fine; -(123i) is not
- Overflowing integer literals are a parse error.
- Only
==is defined. No+,-,<, no metamethods. - No implicit conversion either way. Passing an
integerwhere anumberis expected is a type error, and mixing them in an operation is not supported.
local x = integer.create(42) -- number -> integer?, nil if not exact
local y = integer.fromstring("ff", 16) -- -> integer?
local n = integer.tonumber(x) -- may lose precision
local sum = integer.add(a, b) -- wraps (two's complement)
local quot = integer.div(a, b) -- truncated; errors on /0 and on minsigned/-1
local fl = integer.idiv(a, b) -- floored
local rem = integer.rem(a, b)
local mod = integer.mod(a, b)
local ud = integer.udiv(a, b) -- 'u' prefix = unsigned interpretation
if integer.lt(a, b) then end -- signed compare
if integer.ult(a, b) then end -- unsigned compare
local m = integer.band(a, b) -- bnot/band/bor/bxor/btest
local s = integer.lshift(a, 4i) -- lshift/rshift/arshift/lrotate/rrotate
local w = integer.extract(a, 0i, 8i) -- extract/replace
local z = integer.countlz(a) -- countlz/countrz/bswap
print(integer.maxsigned, integer.minsigned)
Companion additions: buffer.readinteger / buffer.writeinteger; string.format accepts
integers for d/i/* (signed) and o/u/x/X (unsigned) but errors for e/f/g;
tostring prints without the i suffix; rawequal compares integers; types.integer in type
functions; C API lua_pushinteger64 / lua_tointeger64 / lua_isinteger64 /
luaL_checkinteger64 / luaL_optinteger64.
string.pack/unpack are deliberately not extended.
5. Standard library differences
5.1 Additions over Lua 5.1
| Library | Luau-only additions |
|---|---|
| globals | typeof, newproxy (restricted), gcinfo |
math |
clamp, sign, round, noise, isnan, isinf, isfinite, lerp, map; log(n, base) (5.2) |
table |
create, find, clear, freeze, isfrozen, clone; move (5.3) |
string |
split; pack/unpack/packsize (5.3, fixed sizes) |
coroutine |
close (5.4), isyieldable (5.3) |
bit32 |
whole library (5.2), plus Luau-only countlz, countrz, byteswap |
utf8 |
whole library (5.3) |
buffer |
whole library (Luau-only) |
vector |
whole library (Luau-only) |
integer |
whole library 🚧 (Luau-only) |
debug |
info, traceback (see below — this is a replacement, not an addition) |
Worth calling out:
-- table.create: preallocate, huge win in hot loops
local t = table.create(1000) -- capacity only, #t == 0
local z = table.create(1000, 0) -- 1000 zeros
-- table.freeze / isfrozen: real immutability, the basis of sandboxing
local cfg = table.freeze({ debug = false })
cfg.debug = true -- error
print(table.isfrozen(cfg)) --> true
-- table.clone: shallow copy preserving the metatable, never frozen
local copy = table.clone(cfg)
-- math.lerp guarantees lerp(a, b, 1) == b exactly
print(math.lerp(0, 10, 0.5)) --> 5
print(math.map(5, 0, 10, 0, 100)) --> 50
-- string.split
print(table.concat(string.split("a,b,c", ","), "|")) --> a|b|c
math.random uses PCG (Lua 5.4 uses Xoroshiro); math.randomseed(seed) gives a deterministic
sequence.
5.2 debug is a different library
Lua 5.1's introspection API is gone. Luau provides two safe functions instead:
local src, line, name = debug.info(2, "sln") -- s=source l=line n=name f=function a=arity
print(debug.traceback("context", 1))
No debug.getinfo, getlocal, setlocal, getupvalue, setupvalue, sethook, getregistry.
5.3 os is trimmed
Kept: os.clock, os.date, os.difftime, os.time.
Removed: os.execute, os.exit, os.getenv, os.remove, os.rename, os.tmpname, os.setlocale.
⚠️ os.time(table) returns a UTC timestamp in Luau; Lua 5.1 interprets the table as local time.
6. Removed from Lua 5.1 (sandboxing)
In AntOS, the filesystem/system removals below are replaced by AntOS's own
io/os/fslibraries (DBFS-backed, drive-letter paths) — so AntOS scripts do have files and storage; they use AntOS's versions, not stock Lua's.
| Removed | Why |
|---|---|
io library |
filesystem access |
package library, module() |
filesystem / global namespace pollution |
os (most of it) |
process and filesystem access |
debug (most of it) |
breaks the isolation barrier |
loadfile, dofile |
filesystem access |
loadstring with bytecode, string.dump |
loading untrusted bytecode is a memory-safety hole |
newproxy(table) |
only newproxy() / newproxy(bool) remain |
__gc |
sandboxing + GC cost; use lua_newuserdatadtor on the host side |
getfenv / setfenv are kept (deprecated in 5.2, removed in 5.3) purely for backwards
compatibility.
Embedder-side sandboxing, which has no Lua 5.1 equivalent:
luaL_sandbox(L)marks all builtin library tables read-only — no monkey-patching, not even viarawsetorsetmetatable.luaL_sandboxthread(L)gives each script its own global table chained to the builtin globals via__index, so scripts are isolated from each other.- This "safeenv" state is what unlocks many of Luau's builtin-call optimizations.
7. Behavioural / semantic differences
These are the ones that bite when porting.
⚠️ No tail calls. return f() grows the stack. Dropped to simplify the implementation, keep
stack traces predictable, and allow deep validation of caller identity. Rewrite deep tail
recursion as a loop.
⚠️ __eq is called even when the operands are rawequal. Lua 5.1 short-circuits on pointer
equality; Luau does not (this matches __lt/__le and makes NaN-style checks expressible).
⚠️ Table literal assignment order. In mixed tables Luau assigns in program order; Lua 5.x sometimes does array elements first.
⚠️ Closure identity. function() ... end may return a previously created closure when all
captured upvalues are identical. Call semantics are unchanged, but object identity is not stable —
don't use closures as table keys expecting freshness. (Same as Lua 5.2/5.3, different from 5.1.)
⚠️ os.time(table) is UTC (see §5.3).
No integer/float distinction — one number type, so no math.type, no 1//1 producing an
integer, no %d overflow surprises from 5.3.
No yielding in metamethods (__index, __newindex, __iter, …), and no yielding from most C
callbacks. pcall/xpcall can yield (5.2 feature, supported). As of 0.722 generalized-iteration
generator functions can yield.
Errors preserve non-string error objects through pcall, as in later Lua versions.
8. Implementation limits
| Limit | Lua 5.1 | Luau |
|---|---|---|
| Local variables / function | 200 | 200 |
| Upvalues / function | 60 | 200 |
| Registers / function | 255 | 255 |
| Constants / function | 2^18 | 2^23 |
| Instructions / function | 2^17 | 2^23 |
| Nested functions / function | 2^18 | 2^15 (lower) |
| Lua call depth | tunable | 20000 per thread |
| C call depth | tunable | 200 (so pcall/coroutine.resume nesting is capped at 200) |
9. Lua 5.2 / 5.3 / 5.4 / 5.5 feature status
Legend: ✔️ in Luau · ❌ rejected · 😞 wanted but breaks compatibility · 🔜 planned · 🤷 no strong opinion
Lua 5.2
| Feature | Status | Note |
|---|---|---|
yieldable pcall/xpcall |
✔️ | |
yieldable xpcall handler |
❌ | VM complexity |
| yieldable metamethods | ❌ | significant perf cost |
| ephemeron tables | ❌ | GC cost; maybe after GC rework |
| emergency GC | 🤷 | |
goto |
❌ | unstructured control flow, compiler complexity |
__gc for tables |
❌ | sandboxing |
| removal of fenv | 😞 | breaks compatibility |
__len on tables |
✔️ | |
hex and \z escapes |
✔️ | |
| hexadecimal floats | 🤷 | |
__lt/__le across unrelated metatables |
❌ | |
empty statement ;; |
🤷 | |
break mid-block |
🤷 | |
args for xpcall'd function |
✔️ | |
math.log(x, base) |
✔️ | |
separator in string.rep |
🤷 | |
__pairs / __ipairs |
❌ | superseded by __iter |
frontier patterns, %g, \0 in patterns |
✔️ | |
bit32 |
✔️ | |
stricter gsub % handling |
✔️ | |
| light C functions | 😞 | |
NaN keys with __newindex |
✔️ |
Lua 5.3
| Feature | Status | Note |
|---|---|---|
\u escapes |
✔️ | |
| 64-bit integer subtype | ❌ | but see the separate integer type in §4.3 |
| bitwise operators | ❌ | bit32 (and integer.*) instead |
utf8 library |
✔️ | |
string.pack/unpack/packsize |
✔️ | fixed sizes: short 16, int 32, long 64, size_t 32 |
| floor division | ✔️ | |
ipairs/table respecting metamethods |
❌ | perf |
table.move |
✔️ | |
collectgarbage("count") single result |
✔️ | |
coroutine.isyieldable |
✔️ | |
stricter table.insert/remove checks |
😞 | |
__eq across unrelated metatables |
❌ |
Lua 5.4
| Feature | Status | Note |
|---|---|---|
| generational GC | 🤷 | other GC optimizations done instead |
<close> |
❌ | no file/socket APIs to close; high complexity |
<const> |
❌ as syntax | equivalent available as const x = v (§2.9) |
new math.random |
✔️ | PCG rather than Xoroshiro |
init arg to string.gmatch |
🤷 | |
coroutine.close / lua_resetthread |
✔️ | |
| string→number coercion moved to string lib | 😞 | |
%p in string.format |
🤷 | |
utf8 codepoints up to 2^31 |
🤷 | |
__lt emulating __le removed |
❌ | |
non-function __gc |
❌ | |
print calls __tostring |
✔️ | |
| utf8 decoders reject surrogates | ✔️ |
Lua 5.5
| Feature | Status | Note |
|---|---|---|
| global variable declarations | ❌ | typechecker catches this |
| named vararg tables | 🤷 | |
| read-only for-loop variables | ❌ | breaks compatibility |
| shortest round-trip float printing | ✔️ | |
table.create(arraysize, recordsize) |
❌ | conflicts with Luau's existing table.create |
utf8.offset returning final position |
🤷 | |
| external strings | ❌ | incompatible object layout |
luaL_openselectedlibs, luaL_makeseed |
🤷 |
10. C API and embedding differences
The API "mostly preserves" Lua 5.1, so existing bindings largely port, with these caveats:
- Compilation is separate from loading. You call
luau_compile()to produce bytecode andluau_load()to load it — this lets you ship a VM with no compiler. There is noluaL_loadstringequivalent that takes source in the runtime. - No
__gc. Uselua_newuserdatadtorfor userdata destructors. - Sandboxing entry points:
luaL_sandbox,luaL_sandboxthread. - Tagged userdata with fast type checks and
lua_setuserdatametatable. (0.725 made the GC visit cached tagged-userdata metatables so they are not collected prematurely.) - 0.725: automatic stack reservation. The C API now auto-reserves required stack slots, so manual
lua_checkstackbookkeeping is largely unnecessary. - 0.725:
luaL_pcallyieldable— yieldable protected C calls for custom libraries. - Recently promoted to the public API:
lua_atbreakpoint,lua_allocationrate,LUA_GCISPAUSEDforlua_gc,lua_memorydump,lua_setpointerencodekey,lua_callhook. - Build options:
LUA_VECTOR_SIZE=4for 4-wide vectors; double-precision vector mode (heap-allocated). - Native code generation is a separate
Luau.CodeGenmodule; 0.727 added a bytecode-to-bytecode JIT inliner (Luau::JitInliner::setup(L),--jit-inlinerin the CLI). - Integer C API additions per §4.3.
11. Modules, require, and .luaurc
Lua 5.1's package/require machinery is gone. Luau uses string-based requires with a
resolution scheme defined by RFC:
local sibling = require("./sibling")
local parent = require("../shared/util")
local aliased = require("@modules/foo")
local own = require("@self/foo") -- only meaningful inside init.luau
- Paths must start with
./,../or@. A barerequire("foo")is an error. (In AntOS, built-in libraries are the exception —require("fs"),require("terminal")are bare, host-registered names; see AntOS libraries.) - A directory containing
init.luauis itself a module. - ⚠️ Inside
init.luau,./fooresolves relative to the parent directory, not the init file. Use@self/footo reach siblings inside the package.@selfis reserved and cannot be overridden. - Aliases come from
.luaurc:
{
"languageMode": "strict",
"lint": { "*": true },
"aliases": {
"modules": "./some/long/path/to/modules"
}
}
.luaurc files are found by walking up the directory tree; closer files override further ones.
There is also an RFC for .config.luau (executable Luau config, evaluated in an isolated VM with a
2-second default timeout); .luaurc and .config.luau in the same directory is an error.
12. Tooling
Lua 5.1 ships lua and luac. Luau ships:
luau— REPL / script runner, sandboxed (no filesystem access beyondrequire). Since 0.726 it will try.luauthen.luaextensions before erroring.luau-analyze— type checker and linter.--solver=new|old,--outputfor diagnostics to a file (0.727).luau-compile— bytecode dumper.luau-lsp— community language server front-end.- A rich linter with its own warning set (unused variables, shadowing, deprecated calls,
suspicious globals, etc.), configurable per-file via
--!comments or per-tree via.luaurc. - Lute — a separate standalone general-purpose Luau runtime (luau-lang/lute), tracking Luau releases closely.
13. Recent changes: 0.720 → 0.730
Weekly releases. Highlights that affect the language rather than internals:
0.730 (17 Jul 2026)
@deprecatedpropagated into autocomplete entries.Frontend::clearModules()for selective module removal.- Fixed a regression inferring
{ unknown }for array-tables built withtable.insert. - Fixed loops "cancelling out" refinements established earlier in the block.
math.roundon negative zero in SSE 4.1 mode.- NCG: better register-spill handling and diagnostics.
(0.729 is not present in the release list; lute nightlies referenced it, so treat 0.728 → 0.730 as contiguous for language purposes.)
0.728 (3 Jul 2026)
- Improved inference around table-literal function arguments.
- NCG: fixed rare data corruption in the
integerlibrary'smod/div/remfamily — the first release note confirmingintegerin the tree.
0.727 (26 Jun 2026)
- JIT bytecode-to-bytecode inliner (
Luau::JitInliner::setup,--jit-inliner). - Variadic type annotations are now pushed into arguments — a lambda assigned to a field typed
(...string) -> ()getsstringparams instead ofunknown. extern classdeclaration syntax disallowed (cleanup towarddeclare extern type).- Bidirectional return-type inference disabled when the target function type is ambiguous.
- Generic-bound fix for
nil <: T?.
0.726 (19 Jun 2026)
- Union subtyping failures now report one abbreviated message rather than one per union member.
- Cyclic type alias assertion fix; requirers of a broken module see an error type instead of cascading type-function errors.
- CST support for attribute lists and parameterized attributes.
0.725 (12 Jun 2026)
- C API auto-reserves stack slots (no more manual
lua_checkstack). luaL_pcallyieldable.- GC visits cached tagged userdata metatables.
exportcrash fixes;Library.table.unpackno longer inferred as...unknown.
0.724 (8 Jun 2026)
ConstraintGraphabstraction in the solver.type:issubtypeoftype function implemented.- Compiler: constant table folding, table function expression inlining, bytecode call inlining.
0.723 (29 May 2026)
exportvalue semantics implemented (RFC 179) — §2.10.
0.722 (22 May 2026)
- Yielding iterators — generalized iteration generators may yield. Metamethods including
__iterstill may not. - CST preserves expression and type groups (parentheses/whitespace).
- New
CMPPROTObytecode instruction for JIT inlining.
0.721 (15 May 2026)
- Read-only indexers
{read T}/{read [K]: V}— §3.4. - First implementation steps for classes — §2.12.
--solver=new|oldinluau-analyze(new is default).CALLFBinstruction + feedback vectors for call-site inlining decisions.
0.720 (8 May 2026)
constappears in autocomplete.- Fixed false
OptionalValueAccesswhen iterating a table with an optional indexer type. - Better bidirectional inference for unions of tables and functions.
- Deprecation notice:
declare classis being removed; migrate definition files todeclare extern type Foo with ... end.
14. Porting checklist
Moving Lua 5.1 code to Luau:
- Search for
loadstring,dofile,loadfile,require "name",io.,os.execute,os.getenv,package.,module(,debug.getinfo,setmetatable(..., {__gc=...}). All of these need rework. - Check for deep tail recursion — no tail calls.
- Check
__eqimplementations for assumptions about rawequal short-circuiting. - Check
os.time(t)— now UTC. - Anything relying on closure identity (closures as table keys,
==on freshly created functions) may behave differently. - Bytecode caching —
string.dumpis gone; precompile withluau_compileon the host. - Then start opting in: add
--!strictper file, replacepairs(t)withfor k, v in t, replacea and b or cwithif..then..else,table.inserthot loops withtable.createpreallocation, byte-packing code withbuffer.
Generated 28 August 2026. Luau moves weekly — check github.com/luau-lang/luau/releases and rfcs.luau.org for anything after 0.730.